diff --git a/files-api/src/main/java/org/xbib/files/DefaultFileService.java b/files-api/src/main/java/org/xbib/files/DefaultFileService.java index 4d55a74..e0a87e1 100644 --- a/files-api/src/main/java/org/xbib/files/DefaultFileService.java +++ b/files-api/src/main/java/org/xbib/files/DefaultFileService.java @@ -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 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 dirPerms, + Set 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 dirPerms, + Set 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 dirPerms, + Set 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 dirPerms, + Set 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 dirPerms, - Set filePerms, - CopyOption... copyOptions) throws IOException { - upload(Files.newByteChannel(source), target, WRITE_BUFFER_SIZE, dirPerms, filePerms, copyOptions); - } - - private void upload(InputStream source, Path target, - Set dirPerms, - Set 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 dirPerms, Set 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(); diff --git a/files-api/src/main/java/org/xbib/files/FileService.java b/files-api/src/main/java/org/xbib/files/FileService.java index d6643e3..7302028 100644 --- a/files-api/src/main/java/org/xbib/files/FileService.java +++ b/files-api/src/main/java/org/xbib/files/FileService.java @@ -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 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 dirPermissions, + Set 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 dirPermissions, + Set 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 dirPermissions, + Set 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 dirPermissions, + Set 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; diff --git a/files-ftp-fs/build.gradle b/files-ftp-fs/build.gradle index 8528be1..f52a6e1 100644 --- a/files-ftp-fs/build.gradle +++ b/files-ftp-fs/build.gradle @@ -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' diff --git a/files-ftp-fs/src/main/java/module-info.java b/files-ftp-fs/src/main/java/module-info.java index 5c8e419..6945aee 100644 --- a/files-ftp-fs/src/main/java/module-info.java +++ b/files-ftp-fs/src/main/java/module-info.java @@ -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; } diff --git a/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFiles.java b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFileService.java similarity index 75% rename from files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFiles.java rename to files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFileService.java index 10fb54e..cdc89c0 100644 --- a/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFiles.java +++ b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFileService.java @@ -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 DEFAULT_DIR_PERMISSIONS = PosixFilePermissions.fromString("rwxr-xr-x"); @@ -47,7 +46,7 @@ public class FTPFiles implements FileService { private final Map env; - public FTPFiles(URI uri, Map env) { + public FTPFileService(URI uri, Map 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 dirPerms, + Set 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 dirPerms, + Set 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 dirPerms, + Set 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 dirPerms, + Set 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 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 dirPerms, - Set 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 dirPerms, - Set 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 dirPerms, - Set 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 dirPerms, - Set 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 dirPerms, Set 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(); diff --git a/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFilesProvider.java b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFileServiceProvider.java similarity index 73% rename from files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFilesProvider.java rename to files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFileServiceProvider.java index 7dac0a4..ea262af 100644 --- a/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFilesProvider.java +++ b/files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/spi/FTPFileServiceProvider.java @@ -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 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; } } diff --git a/files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider b/files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider index 32d37d1..329801d 100644 --- a/files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider +++ b/files-ftp-fs/src/main/resources/META-INF/services/org.xbib.files.FileServiceProvider @@ -1 +1 @@ -org.xbib.io.ftp.fs.spi.FTPFilesProvider \ No newline at end of file +org.xbib.io.ftp.fs.spi.FTPFileServiceProvider \ No newline at end of file diff --git a/files-ftp-fs/src/test/java/module-info.java b/files-ftp-fs/src/test/java/module-info.java index e5b8d8e..efb2a5d 100644 --- a/files-ftp-fs/src/test/java/module-info.java +++ b/files-ftp-fs/src/test/java/module-info.java @@ -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; diff --git a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/AbstractFTPFileSystemTest.java b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/AbstractFTPFileSystemTest.java index 2cbf811..f24aeb3 100644 --- a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/AbstractFTPFileSystemTest.java +++ b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/AbstractFTPFileSystemTest.java @@ -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; diff --git a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemInputStreamTest.java b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemInputStreamTest.java index 1bc15c5..036baf4 100644 --- a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemInputStreamTest.java +++ b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemInputStreamTest.java @@ -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 { diff --git a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemOutputStreamTest.java b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemOutputStreamTest.java index d3eaa37..ebef7bb 100644 --- a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemOutputStreamTest.java +++ b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemOutputStreamTest.java @@ -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 { diff --git a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemProviderTest.java b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemProviderTest.java index 5a07c6a..d1e8bae 100644 --- a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemProviderTest.java +++ b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemProviderTest.java @@ -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; diff --git a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemTest.java b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemTest.java index 9b1d2b1..fcfddfc 100644 --- a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemTest.java +++ b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/FTPFileSystemTest.java @@ -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; diff --git a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/ExtendedUnixDirectoryListingFormatter.java b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/ExtendedUnixDirectoryListingFormatter.java index 92aaabb..d46233d 100644 --- a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/ExtendedUnixDirectoryListingFormatter.java +++ b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/ExtendedUnixDirectoryListingFormatter.java @@ -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. diff --git a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/ExtendedUnixFakeFileSystem.java b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/ExtendedUnixFakeFileSystem.java index 138faf7..a2c9189 100644 --- a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/ExtendedUnixFakeFileSystem.java +++ b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/ExtendedUnixFakeFileSystem.java @@ -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. diff --git a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/ListHiddenFilesCommandHandler.java b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/ListHiddenFilesCommandHandler.java index 6ff1f79..12a5c21 100644 --- a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/ListHiddenFilesCommandHandler.java +++ b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/ListHiddenFilesCommandHandler.java @@ -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(); diff --git a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/MDTMCommandHandler.java b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/MDTMCommandHandler.java index fec9801..7cefd8a 100644 --- a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/MDTMCommandHandler.java +++ b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/MDTMCommandHandler.java @@ -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. diff --git a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/SymbolicLinkEntry.java b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/SymbolicLinkEntry.java index 64741d8..624eb97 100644 --- a/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/SymbolicLinkEntry.java +++ b/files-ftp-fs/src/test/java/org/xbib/io/ftp/fs/test/server/SymbolicLinkEntry.java @@ -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. diff --git a/files-ftp-mock/build.gradle b/files-ftp-mock/build.gradle new file mode 100644 index 0000000..7c228a0 --- /dev/null +++ b/files-ftp-mock/build.gradle @@ -0,0 +1,5 @@ +dependencies { + testImplementation testLibs.junit.jupiter.params + testImplementation project(':files-ftp') + testImplementation testLibs.mockito.core +} diff --git a/files-ftp-mock/src/main/java/module-info.java b/files-ftp-mock/src/main/java/module-info.java new file mode 100644 index 0000000..7fa340d --- /dev/null +++ b/files-ftp-mock/src/main/java/module-info.java @@ -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; +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/CommandSyntaxException.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/CommandSyntaxException.java new file mode 100644 index 0000000..e2bd87e --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/CommandSyntaxException.java @@ -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); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/IllegalStateException.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/IllegalStateException.java new file mode 100644 index 0000000..aafc701 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/IllegalStateException.java @@ -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); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/MockFtpServerException.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/MockFtpServerException.java new file mode 100644 index 0000000..a96cf40 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/MockFtpServerException.java @@ -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); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/NotLoggedInException.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/NotLoggedInException.java new file mode 100644 index 0000000..45cd028 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/NotLoggedInException.java @@ -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); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/AbstractCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/AbstractCommandHandler.java new file mode 100644 index 0000000..e9ee66a --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/AbstractCommandHandler.java @@ -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"); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/AbstractStaticReplyCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/AbstractStaticReplyCommandHandler.java new file mode 100644 index 0000000..07bb035 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/AbstractStaticReplyCommandHandler.java @@ -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 replyCode property. If only the + * replyCode 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 replyMessageKey or replyText property. + * + *

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); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/AbstractTrackingCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/AbstractTrackingCommandHandler.java new file mode 100644 index 0000000..c0e8424 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/AbstractTrackingCommandHandler.java @@ -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. + * + *

The reply code is designated by the replyCode property, and the reply text + * is determined by the following rules: + *

    + *
  1. If the replyText property is non-null, then use that.
  2. + *
  3. Otherwise, if replyMessageKey is non-null, the use that to retrieve a + * localized message from the replyText ResourceBundle.
  4. + *
  5. Otherwise, retrieve the reply text from the replyText ResourceBundle, + * using the reply code as the key.
  6. + *
+ * 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 + * numberOfInvocations() 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. + * + *

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; + } + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/Command.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/Command.java new file mode 100644 index 0000000..3a7f692 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/Command.java @@ -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); + } + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/CommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/CommandHandler.java new file mode 100644 index 0000000..7c1aeec --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/CommandHandler.java @@ -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; + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/CommandNames.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/CommandNames.java new file mode 100644 index 0000000..aaabdb5 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/CommandNames.java @@ -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() { + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/ConnectCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/ConnectCommandHandler.java new file mode 100644 index 0000000..dc8a5cb --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/ConnectCommandHandler.java @@ -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. + * + *

Note that this is a "special" CommandHandler, in that it handles the initial connection from the + * client, rather than an explicit FTP command. + * + *

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); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/InvocationHistory.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/InvocationHistory.java new file mode 100644 index 0000000..135af9b --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/InvocationHistory.java @@ -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 + * numberOfInvocations() method will return zero. + */ + void clearInvocations(); + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/InvocationRecord.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/InvocationRecord.java new file mode 100644 index 0000000..9d0dae8 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/InvocationRecord.java @@ -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 (InetAddress) of the client that submitted the + * Command and the timestamp of the Command submission. + * + *

This class also supports storing zero or more arbitrary mappings of key to value, where key is + * a String and value is any Object. Convenience methods are provided that enable retrieving + * type-specific data by its key. The data stored in an {@link InvocationRecord} is CommandHandler-specific. + * + *

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 AssertFailedException. + * + * @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 + * AssertFailedException. + */ + 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 AssertFailedException 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 true if this object contains a mapping for the specified key. + * + * @param key - the key; must not be null + * @return true 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 + "]"; + } +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/ReplyCodes.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/ReplyCodes.java new file mode 100644 index 0000000..c3b463e --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/ReplyCodes.java @@ -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() { + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/ReplyTextBundleAware.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/ReplyTextBundleAware.java new file mode 100644 index 0000000..ebbbd46 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/ReplyTextBundleAware.java @@ -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); + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/ReplyTextBundleUtil.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/ReplyTextBundleUtil.java new file mode 100644 index 0000000..304fe72 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/ReplyTextBundleUtil.java @@ -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 replyTextBundle property of the specified CommandHandler to the + * ResourceBundle if and only if the commandHandler implements the + * {@link ReplyTextBundleAware} interface AND its replyTextBundle 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); + } + } + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/SimpleCompositeCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/SimpleCompositeCommandHandler.java new file mode 100644 index 0000000..d950742 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/SimpleCompositeCommandHandler.java @@ -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. + * + *

The following example replaces the CWD CommandHandler with a SimpleCompositeCommandHandler. + * The first invocation of the CWD command will fail (reply code 500). The seconds will succeed. + *


+ * 
+ * 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);
+ * 
+ * + * @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 setReplyTextBundle() 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); + } + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/StaticReplyCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/StaticReplyCommandHandler.java new file mode 100644 index 0000000..8a1a18f --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/StaticReplyCommandHandler.java @@ -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 replyCode property. If only the + * replyCode 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 replyMessageKey or replyText property. + * + *

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); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/UnsupportedCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/UnsupportedCommandHandler.java new file mode 100644 index 0000000..57b6e18 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/command/UnsupportedCommandHandler.java @@ -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. + * + *

Note that this is a "special" CommandHandler, in that it handles any unrecognized command, + * rather than an explicit FTP command. + * + *

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()); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/server/AbstractFtpServer.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/server/AbstractFtpServer.java new file mode 100644 index 0000000..9a40e6d --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/server/AbstractFtpServer.java @@ -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. + * + *

By default, mock FTP Servers bind to the server control port of 21. You can use a different server control + * port by setting the serverControlPort property. If you specify a value of 0, + * then a free port number will be chosen automatically; call getServerControlPort() AFTER + * start() 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. + * + *

Command Handlers

+ * 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 Spring Framework. + * + *

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. + * + *

FTP Command Reply Text ResourceBundle

+ * 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 commandHandlerMapping. 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 replyTextBundle attribute + * is null, then set its replyTextBundle to the replyTextBundle 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); + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/session/DefaultSession.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/session/DefaultSession.java new file mode 100644 index 0000000..531939f --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/session/DefaultSession.java @@ -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 + *

+ * 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); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/session/Session.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/session/Session.java new file mode 100644 index 0000000..9d1b46f --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/session/Session.java @@ -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(); + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/session/SessionKeys.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/session/SessionKeys.java new file mode 100644 index 0000000..e19a287 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/session/SessionKeys.java @@ -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"; + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/socket/DefaultServerSocketFactory.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/socket/DefaultServerSocketFactory.java new file mode 100644 index 0000000..14a0bf4 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/socket/DefaultServerSocketFactory.java @@ -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); + } +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/socket/DefaultSocketFactory.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/socket/DefaultSocketFactory.java new file mode 100644 index 0000000..3526e02 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/socket/DefaultSocketFactory.java @@ -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); + } +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/socket/ServerSocketFactory.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/socket/ServerSocketFactory.java new file mode 100644 index 0000000..3c6a9d8 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/socket/ServerSocketFactory.java @@ -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; +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/socket/SocketFactory.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/socket/SocketFactory.java new file mode 100644 index 0000000..2dbb662 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/socket/SocketFactory.java @@ -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; +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/Assert.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/Assert.java new file mode 100644 index 0000000..943bc68 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/Assert.java @@ -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 + * AssertFailedException 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() { + } +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/AssertFailedException.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/AssertFailedException.java new file mode 100644 index 0000000..7bdb982 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/AssertFailedException.java @@ -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); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/HostAndPort.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/HostAndPort.java new file mode 100644 index 0000000..cde5449 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/HostAndPort.java @@ -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; + } +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/IoUtil.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/IoUtil.java new file mode 100644 index 0000000..335e2d4 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/IoUtil.java @@ -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() { + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/PatternUtil.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/PatternUtil.java new file mode 100644 index 0000000..59c8c8c --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/PatternUtil.java @@ -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() { + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/PortParser.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/PortParser.java new file mode 100644 index 0000000..84a04b9 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/PortParser.java @@ -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. + * + *

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() { + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/StringUtil.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/StringUtil.java new file mode 100644 index 0000000..d1fbc62 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/core/util/StringUtil.java @@ -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() { + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/FakeFtpServer.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/FakeFtpServer.java new file mode 100644 index 0000000..9169364 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/FakeFtpServer.java @@ -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; + +/** + * FakeFtpServer 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. + * + *

FakeFtpServer 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. + * + *

FakeFtpServer 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. + * + *

FakeFtpServer can be fully configured programmatically or within the + * Spring Framework or other dependency-injection container. + * + *

In general the steps for setting up and starting the FakeFtpServer are: + *

    + *
  1. Create a new FakeFtpServer instance, and optionally set the server control port.
  2. + *
  3. Create and configure a FileSystem, and attach to the FakeFtpServer instance.
  4. + *
  5. Create and configure one or more UserAccount objects and attach to the FakeFtpServer instance.
  6. + *
  7. Start the FakeFtpServer instance.
  8. + *
+ *

Example 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();
+ * 
+ * + *

Example Code with Permissions

+ * You can optionally set the permissions and owner/group for each file and directory, as in the following example. + *

+ * 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();
+ * 
+ * + *

FTP Server Control Port

+ * By default, FakeFtpServer binds to the server control port of 21. You can use a different server control + * port by setting the serverControlPort property. If you specify a value of 0, + * then a free port number will be chosen automatically; call getServerControlPort() AFTER + * start() 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. + * + *

Other Configuration

+ * The systemName property specifies the value returned by the SYST + * 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 systemName property is set, then that + * will override the FileSystem value. + * + *

The helpText property specifies a Map of help text replies sent by the + * HELP command. The keys in that Map correspond to the command names passed as + * parameters to the HELP 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 HELP command. + * + *

FTP Command Reply Text ResourceBundle

+ * 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 ServerConfigurationAware interface, then set its + * ServerConfiguration property to this. + * + * If the CommandHandler implements the ReplyTextBundleAware interface, then set its + * replyTextBundle 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 username 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 userAccountList 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; + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/ServerConfiguration.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/ServerConfiguration.java new file mode 100644 index 0000000..69d92e6 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/ServerConfiguration.java @@ -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); + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/ServerConfigurationAware.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/ServerConfigurationAware.java new file mode 100644 index 0000000..127eb54 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/ServerConfigurationAware.java @@ -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); +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/UserAccount.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/UserAccount.java new file mode 100644 index 0000000..94a9126 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/UserAccount.java @@ -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. + * + *

The username and homeDirectory property must be non-null + * and non-empty. The homeDirectory property must also match the name of an existing + * directory within the file system configured for the FakeFtpServer. + * + *

The group name applied to newly created files/directories is determined by the groups property. + * If null or empty, then the default group name ("users") is used. Otherwise, the first value in the + * groups List is used. The groups property defaults to an empty List. + * + *

The default value for defaultPermissionsForNewFile is read and write permissions for + * all (user/group/world). The default value for defaultPermissionsForNewDirectory is read, + * write and execute permissions for all (user/group/world). + * + *

The isValidPassword() method returns true if the specified password matches + * the password value configured for this user account. This implementation uses the + * isEquals() method to compare passwords. + * + *

If you want to provide a custom comparison, for instance using encrypted passwords, you can + * subclass this class and override the comparePassword() method to provide your own + * custom implementation. + * + *

If the passwordCheckedDuringValidation property is set to false, then the password + * value is ignored, and the isValidPassword() method just returns true. + * + *

The accountRequiredForLogin 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 DEFAULT_GROUP. Otherwise, this method + * returns the first group name in the groups 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)); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AborCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AborCommandHandler.java new file mode 100644 index 0000000..e327a58 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AborCommandHandler.java @@ -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: + *

    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. Otherwise, reply with 226
  4. + *
+ * + * @author Chris Mair + */ +public class AborCommandHandler extends AbstractFakeCommandHandler { + + protected void handle(Command command, Session session) { + verifyLoggedIn(session); + sendReply(session, ReplyCodes.ABOR_OK, "abor"); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AbstractFakeCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AbstractFakeCommandHandler.java new file mode 100644 index 0000000..45cf37d --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AbstractFakeCommandHandler.java @@ -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. + * + *

The reply code is designated by the replyCode property, and the reply text + * is retrieved from the replyText 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. + * + *

The reply code is designated by the replyCode property, and the reply text + * is retrieved from the replyText 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. + * + *

The reply code is designated by the replyCode property, and the reply text + * is retrieved from the replyText 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. + * + *

The reply code is designated by the replyCode property, and the reply text + * is retrieved from the replyText 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); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AbstractStoreFileCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AbstractStoreFileCommandHandler.java new file mode 100644 index 0000000..a145f42 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AbstractStoreFileCommandHandler.java @@ -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: + *

    + *
  1. If the user has not logged in, then reply with 530 and terminate
  2. + *
  3. If the pathname parameter is required but missing, then reply with 501 and terminate
  4. + *
  5. If the required pathname parameter does not specify a valid filename, then reply with 553 and terminate
  6. + *
  7. 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
  8. + *
  9. If the current user does not have execute access to the parent directory, then reply with 553 and terminate
  10. + *
  11. Send an initial reply of 150
  12. + *
  13. Read all available bytes from the data connection and store/append to the named file in the server file system
  14. + *
  15. If file write/store fails, then reply with 553 and terminate
  16. + *
  17. Send a final reply with 226
  18. + *
+ * + * @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(); + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AcctCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AcctCommandHandler.java new file mode 100644 index 0000000..d5146f0 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AcctCommandHandler.java @@ -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: + *
    + *
  1. If the required account parameter is missing, then reply with 501
  2. + *
  3. If this command was not preceded by a valid USER command, then reply with 503
  4. + *
  5. Store the account name in the session and reply with 230
  6. + *
+ * + * @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)); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AlloCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AlloCommandHandler.java new file mode 100644 index 0000000..2336a44 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AlloCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. Otherwise, reply with 200
  4. + *
+ * + * @author Chris Mair + */ +public class AlloCommandHandler extends AbstractFakeCommandHandler { + + protected void handle(Command command, Session session) { + verifyLoggedIn(session); + sendReply(session, ReplyCodes.ALLO_OK, "allo"); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AppeCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AppeCommandHandler.java new file mode 100644 index 0000000..a3a2f12 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/AppeCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530 and terminate
  2. + *
  3. If the required pathname parameter is missing, then reply with 501 and terminate
  4. + *
  5. If the pathname parameter does not specify a valid filename, then reply with 553 and terminate
  6. + *
  7. 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
  8. + *
  9. If the current user does not have execute access to the parent directory, then reply with 553 and terminate
  10. + *
  11. Send an initial reply of 150
  12. + *
  13. Read all available bytes from the data connection and append to the named file in the server file system
  14. + *
  15. If file write/store fails, then reply with 553 and terminate
  16. + *
  17. Send a final reply with 226
  18. + *
+ * + * @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; + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/CdupCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/CdupCommandHandler.java new file mode 100644 index 0000000..c685e41 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/CdupCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. If the current directory has no parent or if the current directory cannot be changed, then reply with 550 and terminate
  4. + *
  5. If the current user does not have execute access to the parent directory, then reply with 550 and terminate
  6. + *
  7. Otherwise, reply with 200 and change the current directory stored in the session to the parent directory
  8. + *
+ * + * @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)); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/CwdCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/CwdCommandHandler.java new file mode 100644 index 0000000..3cce70a --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/CwdCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. If the required pathname parameter is missing, then reply with 501 and terminate
  4. + *
  5. If the pathname parameter does not specify an existing directory, then reply with 550 and terminate
  6. + *
  7. If the current user does not have execute access to the directory, then reply with 550 and terminate
  8. + *
  9. Otherwise, reply with 250 and change the current directory stored in the session
  10. + *
+ * 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)); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/DeleCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/DeleCommandHandler.java new file mode 100644 index 0000000..436976a --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/DeleCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. If the required pathname parameter is missing, then reply with 501
  4. + *
  5. If the pathname parameter does not specify an existing file then reply with 550
  6. + *
  7. If the current user does not have write access to the parent directory, then reply with 550
  8. + *
  9. Otherwise, delete the named file and reply with 250
  10. + *
+ * 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)); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/EprtCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/EprtCommandHandler.java new file mode 100644 index 0000000..047d9ef --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/EprtCommandHandler.java @@ -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: + *
    + *
  1. 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") + *
  2. Send back a reply of 200
  3. + *
+ * 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"); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/EpsvCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/EpsvCommandHandler.java new file mode 100644 index 0000000..f05b94d --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/EpsvCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. Otherwise, request the Session to switch to passive data connection mode. Return a reply code + * of 229, along with response text including: "(|||PORT|)", where PORT is the 16-bit + * TCP port address of the data connection on the server to which the client must connect.
  4. + *
+ * 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))); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/HelpCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/HelpCommandHandler.java new file mode 100644 index 0000000..f102236 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/HelpCommandHandler.java @@ -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: + *
    + *
  1. 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)
  2. + *
  3. Otherwise, reply with 214 along with the configured default help text that has been configured + * (or empty if none)
  4. + *
+ * + *

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)); + } + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/ListCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/ListCommandHandler.java new file mode 100644 index 0000000..1e8a908 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/ListCommandHandler.java @@ -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: + *

    + *
  1. If the user has not logged in, then reply with 530 and terminate
  2. + *
  3. If the current user does not have read access to the file or directory to be listed, then reply with 550 and terminate
  4. + *
  5. If an error occurs during processing, then send a reply of 451 and terminate
  6. + *
  7. Send an initial reply of 150
  8. + *
  9. If the optional pathname parameter is missing, then send a directory listing for + * the current directory across the data connection
  10. + *
  11. 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
  12. + *
  13. Otherwise, if the optional pathname parameter specifies a filename, then send information + * for the specified file across the data connection
  14. + *
  15. Send a final reply with 226
  16. + *
+ * + * @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); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/MkdCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/MkdCommandHandler.java new file mode 100644 index 0000000..ab853f6 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/MkdCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. If the required pathname parameter is missing, then reply with 501
  4. + *
  5. If the parent directory of the specified pathname does not exist, then reply with 550
  6. + *
  7. If the pathname parameter specifies an existing file or directory, or if the create directory fails, then reply with 550
  8. + *
  9. If the current user does not have write and execute access to the parent directory, then reply with 550
  10. + *
  11. Otherwise, reply with 257
  12. + *
+ * 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)); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/ModeCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/ModeCommandHandler.java new file mode 100644 index 0000000..f5f8a6c --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/ModeCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. Otherwise, reply with 200
  4. + *
+ * + * @author Chris Mair + */ +public class ModeCommandHandler extends AbstractFakeCommandHandler { + + protected void handle(Command command, Session session) { + verifyLoggedIn(session); + sendReply(session, ReplyCodes.MODE_OK, "mode"); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/NlstCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/NlstCommandHandler.java new file mode 100644 index 0000000..458066b --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/NlstCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530 and terminate
  2. + *
  3. If the current user does not have read access to the file or directory to be listed, then reply with 550 and terminate
  4. + *
  5. If an error occurs during processing, then send a reply of 451 and terminate
  6. + *
  7. Send an initial reply of 150
  8. + *
  9. If the optional pathname parameter is missing, then send a directory listing for + * the current directory across the data connection
  10. + *
  11. 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
  12. + *
  13. Otherwise, if the pathname parameter does not specify an existing directory or group of files, + * then send an empty response across the data connection
  14. + *
  15. Send a final reply with 226
  16. + *
+ * 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); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/NoopCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/NoopCommandHandler.java new file mode 100644 index 0000000..542f4c1 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/NoopCommandHandler.java @@ -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: + *
    + *
  1. Reply with 200
  2. + *
+ * + * @author Chris Mair + */ +public class NoopCommandHandler extends AbstractFakeCommandHandler { + + protected void handle(Command command, Session session) { + sendReply(session, ReplyCodes.NOOP_OK, "noop"); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/PassCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/PassCommandHandler.java new file mode 100644 index 0000000..1262aa6 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/PassCommandHandler.java @@ -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: + *
    + *
  1. If the required password parameter is missing, then reply with 501
  2. + *
  3. If this command was not preceded by a valid USER command, then reply with 503
  4. + *
  5. If the user account configured for the named user does not exist or is not valid, then reply with 530
  6. + *
  7. If the specified password is not correct, then reply with 530
  8. + *
  9. Otherwise, reply with 230
  10. + *
+ * + * @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"); + } + } + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/PasvCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/PasvCommandHandler.java new file mode 100644 index 0000000..deb2f0a --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/PasvCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. Otherwise, request the Session to switch to passive data connection mode. Return a reply code of 227, + * along with response text of the form: "(h1,h2,h3,h4,p1,p2)", where h1..h4 are the + * 4 bytes of the 32-bit internet host address of the server, and p1..p2 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.
  4. + *
+ * + * @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)); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/PortCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/PortCommandHandler.java new file mode 100644 index 0000000..60b26e0 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/PortCommandHandler.java @@ -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: + *
    + *
  1. Parse the client data host (InetAddress) submitted from parameters 1-4 + *
  2. Parse the port number submitted on the invocation from parameter 5-6 + *
  3. Send backa a reply of 200
  4. + *
+ * + * @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"); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/PwdCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/PwdCommandHandler.java new file mode 100644 index 0000000..cbc20bc --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/PwdCommandHandler.java @@ -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: + *
    + *
  1. If the required "current directory" property is missing from the session, then reply with 550
  2. + *
  3. Otherwise, reply with 257 and the current directory
  4. + *
+ * + * @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)); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/QuitCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/QuitCommandHandler.java new file mode 100644 index 0000000..4269379 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/QuitCommandHandler.java @@ -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(); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/ReinCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/ReinCommandHandler.java new file mode 100644 index 0000000..e779a57 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/ReinCommandHandler.java @@ -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: + *
    + *
  1. Terminates (logs out) the current user, if there is one
  2. + *
  3. Reply with 220
  4. + *
+ * + * @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"); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RestCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RestCommandHandler.java new file mode 100644 index 0000000..6de2fc5 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RestCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. Otherwise, reply with 350
  4. + *
+ * + * @author Chris Mair + */ +public class RestCommandHandler extends AbstractFakeCommandHandler { + + protected void handle(Command command, Session session) { + verifyLoggedIn(session); + sendReply(session, ReplyCodes.REST_OK, "rest"); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RetrCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RetrCommandHandler.java new file mode 100644 index 0000000..3b95fab --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RetrCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530 and terminate
  2. + *
  3. If the required pathname parameter is missing, then reply with 501 and terminate
  4. + *
  5. If the pathname parameter does not specify a valid, existing filename, then reply with 550 and terminate
  6. + *
  7. 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
  8. + *
  9. Send an initial reply of 150
  10. + *
  11. Send the contents of the named file across the data connection
  12. + *
  13. If there is an error reading the file, then reply with 550 and terminate
  14. + *
  15. Send a final reply with 226
  16. + *
+ * + * @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; + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RmdCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RmdCommandHandler.java new file mode 100644 index 0000000..8e93c50 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RmdCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. If the required pathname parameter is missing, then reply with 501
  4. + *
  5. If the pathname parameter does not specify an existing, empty directory, then reply with 550
  6. + *
  7. If the current user does not have write access to the parent directory, then reply with 550
  8. + *
  9. Otherwise, delete the named directory and reply with 250
  10. + *
+ * 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)); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RnfrCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RnfrCommandHandler.java new file mode 100644 index 0000000..605bc8c --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RnfrCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. If the required FROM pathname parameter is missing, then reply with 501
  4. + *
  5. If the FROM pathname parameter does not specify a valid file or directory, then reply with 550
  6. + *
  7. If the current user does not have read access to the path, then reply with 550
  8. + *
  9. Otherwise, reply with 350 and store the FROM path in the session
  10. + *
+ * 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"); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RntoCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RntoCommandHandler.java new file mode 100644 index 0000000..6a1e15f --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/RntoCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. If this command was not preceded by a valid RNFR command, then reply with 503
  4. + *
  5. If the required TO pathname parameter is missing, then reply with 501
  6. + *
  7. If the TO pathname parameter does not specify a valid filename, then reply with 553
  8. + *
  9. If the TO pathname parameter specifies an existing directory, then reply with 553
  10. + *
  11. If the current user does not have write access to the parent directory, then reply with 553
  12. + *
  13. Otherwise, rename the file, remove the FROM path stored in the session by the RNFR command, and reply with 250
  14. + *
+ * 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)); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/SiteCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/SiteCommandHandler.java new file mode 100644 index 0000000..209648e --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/SiteCommandHandler.java @@ -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: + *
    + *
  1. Reply with 200
  2. + *
+ * + * @author Chris Mair + */ +public class SiteCommandHandler extends AbstractFakeCommandHandler { + + protected void handle(Command command, Session session) { + verifyLoggedIn(session); + sendReply(session, ReplyCodes.SITE_OK, "site"); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/SizeCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/SizeCommandHandler.java new file mode 100644 index 0000000..97ce39c --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/SizeCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530 and terminate
  2. + *
  3. If the required pathname parameter is missing, then reply with 501 and terminate
  4. + *
  5. If the pathname parameter does not specify a valid, existing filename, then reply with 550 and terminate
  6. + *
  7. + * 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 + *
  8. + *
  9. Otherwise, reply with 213
  10. + *
+ * + * @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)); + } +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/SmntCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/SmntCommandHandler.java new file mode 100644 index 0000000..0742c17 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/SmntCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. Otherwise, reply with 250
  4. + *
+ * + * @author Chris Mair + */ +public class SmntCommandHandler extends AbstractFakeCommandHandler { + + protected void handle(Command command, Session session) { + verifyLoggedIn(session); + sendReply(session, ReplyCodes.SMNT_OK, "smnt"); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/StatCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/StatCommandHandler.java new file mode 100644 index 0000000..f7ad36c --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/StatCommandHandler.java @@ -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: + *
    + *
  1. Reply with 211 along with the system status text that has been configured on the + * {@link FakeFtpServer}.
  2. + *
+ * + * @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)); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/StorCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/StorCommandHandler.java new file mode 100644 index 0000000..eb49034 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/StorCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530 and terminate
  2. + *
  3. If the required pathname parameter is missing, then reply with 501 and terminate
  4. + *
  5. If the pathname parameter does not specify a valid filename, then reply with 553 and terminate
  6. + *
  7. 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
  8. + *
  9. If the current user does not have execute access to the parent directory, then reply with 553 and terminate
  10. + *
  11. Send an initial reply of 150
  12. + *
  13. Read all available bytes from the data connection and write out to the named file in the server file system
  14. + *
  15. If file write/store fails, then reply with 553 and terminate
  16. + *
  17. Send a final reply with 226
  18. + *
+ * + * @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"; + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/StouCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/StouCommandHandler.java new file mode 100644 index 0000000..a38f080 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/StouCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530 and terminate
  2. + *
  3. Create new unique filename within the current directory
  4. + *
  5. 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
  6. + *
  7. If the current user does not have execute access to the parent directory, then reply with 553 and terminate
  8. + *
  9. Send an initial reply of 150
  10. + *
  11. Read all available bytes from the data connection and write out to the unique file in the server file system
  12. + *
  13. If file write/store fails, then reply with 553 and terminate
  14. + *
  15. Send a final reply with 226, along with the new unique filename
  16. + *
+ * + * @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; + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/StruCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/StruCommandHandler.java new file mode 100644 index 0000000..7f7c9ce --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/StruCommandHandler.java @@ -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: + *
    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. Otherwise, reply with 200
  4. + *
+ * + * @author Chris Mair + */ +public class StruCommandHandler extends AbstractFakeCommandHandler { + + protected void handle(Command command, Session session) { + verifyLoggedIn(session); + sendReply(session, ReplyCodes.STRU_OK, "stru"); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/SystCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/SystCommandHandler.java new file mode 100644 index 0000000..df0a621 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/SystCommandHandler.java @@ -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: + *
    + *
  1. Reply with 215 along with the system name
  2. + *
+ * The default system name is "WINDOWS", but it can be customized on the + * {@link FakeFtpServer} . + * + *

See the available system names listed in the Assigned Numbers document (RFC 943). + * + * @author Chris Mair + * @see RFC943 + */ +public class SystCommandHandler extends AbstractFakeCommandHandler { + + protected void handle(Command command, Session session) { + sendReply(session, ReplyCodes.SYST_OK, "syst", list(getServerConfiguration().getSystemName())); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/TypeCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/TypeCommandHandler.java new file mode 100644 index 0000000..e78e872 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/TypeCommandHandler.java @@ -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: + *

    + *
  1. If the user has not logged in, then reply with 530
  2. + *
  3. Otherwise, reply with 200
  4. + *
+ * + * @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"); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/UserCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/UserCommandHandler.java new file mode 100644 index 0000000..e4bacfe --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/command/UserCommandHandler.java @@ -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: + *
    + *
  1. If the required pathname parameter is missing, then reply with 501
  2. + *
  3. If the user account configured for the named user is not valid, then reply with 530
  4. + *
  5. 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
  6. + *
  7. Otherwise, set the username in the session and reply with 331
  8. + *
+ * + * @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"); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/AbstractFakeFileSystem.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/AbstractFakeFileSystem.java new file mode 100644 index 0000000..d4e174f --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/AbstractFakeFileSystem.java @@ -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. + * + *

If the createParentDirectoriesAutomatically property is set to true, + * then creating a directory or file will automatically create any parent directories (recursively) + * that do not already exist. If false, then creating a directory or file throws an + * exception if its parent directory does not exist. This value defaults to true. + * + *

The directoryListingFormatter property holds an instance of {@link DirectoryListingFormatter} , + * used by the formatDirectoryListing method to format directory listings in a + * filesystem-specific manner. This property must be initialized by concrete subclasses. + * + *

The systemName 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 true, creating a directory or file will automatically create + * any parent directories (recursively) that do not already exist. If false, + * then creating a directory or file throws an exception if its parent directory + * does not exist. This value defaults to true. + */ + 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 path specifies a filename, + * then this method returns the path of the directory containing that file. If path + * specifies a directory, the this method returns its parent directory. If path is + * empty or does not have a parent component, then return an empty string. + * + *

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 path 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)); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/AbstractFileSystemEntry.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/AbstractFileSystemEntry.java new file mode 100644 index 0000000..27ad0d0 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/AbstractFileSystemEntry.java @@ -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(); + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/DirectoryEntry.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/DirectoryEntry.java new file mode 100644 index 0000000..231b726 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/DirectoryEntry.java @@ -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; + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/DirectoryListingFormatter.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/DirectoryListingFormatter.java new file mode 100644 index 0000000..def169a --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/DirectoryListingFormatter.java @@ -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); + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/FileEntry.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/FileEntry.java new file mode 100644 index 0000000..f750a68 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/FileEntry.java @@ -0,0 +1,195 @@ +/* + * 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.io.*; + +/** + * File system entry representing a file + * + * @author Chris Mair + */ +public class FileEntry extends AbstractFileSystemEntry { + + private static final byte[] EMPTY = new byte[0]; + + private byte[] bytes = EMPTY; + private ByteArrayOutputStream out; + + /** + * Construct a new instance without setting its path + */ + public FileEntry() { + } + + /** + * Construct a new instance with the specified value for its path + * + * @param path - the value for path + */ + public FileEntry(String path) { + super(path); + } + + /** + * Construct a new instance with the specified path and file contents + * + * @param path - the value for path + * @param contents - the contents of the file, as a String + */ + public FileEntry(String path, String contents) { + super(path); + setContents(contents); + } + + /** + * Return false to indicate that this entry represents a file + * + * @return false + */ + public boolean isDirectory() { + return false; + } + + /** + * Return the size of this file + * + * @return the file size in bytes + */ + public long getSize() { + return getCurrentBytes().length; + } + + /** + * Set the contents of the file represented by this entry + * + * @param contents - the String whose bytes are used as the contents + */ + public void setContents(String contents) { + byte[] newBytes = (contents != null) ? contents.getBytes() : EMPTY; + setContentsInternal(newBytes); + } + + /** + * Set the contents of the file represented by this entry, using the specified charset. + * + * @param contents - the String whose bytes are used as the contents + * @param charset - the charset used to convert the string to a byte[] + * + * @throws UnsupportedEncodingException - if an error occurs converting the String to a byte[] using the specified charset + */ + public void setContents(String contents, String charset) throws UnsupportedEncodingException { + byte[] newBytes = (contents != null) ? contents.getBytes(charset) : EMPTY; + setContentsInternal(newBytes); + } + + /** + * Set the contents of the file represented by this entry + * + * @param contents - the byte[] used as the contents + */ + public void setContents(byte[] contents) { + // Copy the bytes[] to guard against subsequent modification of the source array + byte[] newBytes = EMPTY; + if (contents != null) { + newBytes = new byte[contents.length]; + System.arraycopy(contents, 0, newBytes, 0, contents.length); + } + setContentsInternal(newBytes); + } + + /** + * Create and return an InputStream for reading the contents of the file represented by this entry + * + * @return an InputStream + */ + public InputStream createInputStream() { + return new ByteArrayInputStream(getCurrentBytes()); + } + + /** + * Create and return an OutputStream for writing the contents of the file represented by this entry + * + * @param append - true if the OutputStream should append to any existing contents false if + * any existing contents should be overwritten + * @return an OutputStream + * @throws FileSystemException - if an error occurs creating or initializing the OutputStream + */ + public OutputStream createOutputStream(boolean append) { + // If appending and we already have an OutputStream, then continue to use it + if (append && out != null) { + return out; + } + + out = new ByteArrayOutputStream(); + byte[] initialContents = (append) ? bytes : EMPTY; + try { + out.write(initialContents); + } + catch (IOException e) { + throw new FileSystemException(getPath(), null, e); + } + return out; + } + + /** + * 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) { + FileEntry clone = new FileEntry(path); + clone.setLastModified(getLastModified()); + clone.setOwner(getOwner()); + clone.setGroup(getGroup()); + clone.setPermissions(getPermissions()); + clone.setContents(getCurrentBytes()); + return clone; + } + + //------------------------------------------------------------------------- + // Internal Helper Methods + //------------------------------------------------------------------------- + + /** + * @return the current contents of this file entry as a byte[] + */ + private byte[] getCurrentBytes() { + return (out != null) ? out.toByteArray() : bytes; + } + + /** + * Set the contents of the file represented by this entry + * + * @param contents - the byte[] used as the contents + */ + private void setContentsInternal(byte[] contents) { + this.bytes = contents; + + // Get rid of any OutputStream + this.out = null; + } + + /** + * @see Object#toString() + */ + public String toString() { + return "File['" + getPath() + "' size=" + getSize() + " lastModified=" + getLastModified() + " owner=" + + getOwner() + " group=" + getGroup() + " permissions=" + getPermissions() + "]"; + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/FileSystem.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/FileSystem.java new file mode 100644 index 0000000..912eef4 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/FileSystem.java @@ -0,0 +1,167 @@ +/* + * 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.List; + +/** + * Interface for a file system for managing files and directories. + * + * @author Chris Mair + */ +public interface FileSystem { + + /** + * Add the specified file system entry (file or directory) to this file system + * + * @param entry - the FileSystemEntry to add + */ + public void add(FileSystemEntry entry); + + /** + * Return the List of FileSystemEntry objects for the files in the specified directory path. If the + * path does not refer to a valid directory, then an empty List is returned. + * + * @param path - the path of the directory whose contents should be returned + * @return the List of FileSystemEntry objects for all files in the specified directory may be empty + */ + public List listFiles(String path); + + /** + * Return the List of filenames in the specified directory path. The returned filenames do not + * include a path. If the path does not refer to a valid directory, then an empty List is + * returned. + * + * @param path - the path of the directory whose contents should be returned + * @return the List of filenames (not including paths) for all files in the specified directory + * may be empty + * @throws AssertionError - if path is null + */ + public List listNames(String path); + + /** + * 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 AssertionError - if path is null + */ + public boolean delete(String path); + + /** + * 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. + */ + public void rename(String fromPath, String toPath); + + /** + * 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 + */ + public String formatDirectoryListing(FileSystemEntry fileSystemEntry); + + /** + * Return the system name to be returned from the SYST FTP command when using this FileSystem. + * + * @return the system name to be returned from the SYST FTP command when using this FileSystem. + */ + public String getSystemName(); + + //------------------------------------------------------------------------- + // Path-related Methods + //------------------------------------------------------------------------- + + /** + * 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 + */ + public boolean exists(String path); + + /** + * 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 + */ + public boolean isDirectory(String path); + + /** + * 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 + */ + public boolean isFile(String path); + + /** + * Return true if the specified path designates an absolute file path. What + * constitutes an absolute path is dependent on the file system implementation. + * + * @param path - the path + * @return true if path is absolute, false otherwise + * @throws AssertionError - if path is null + */ + public boolean isAbsolute(String path); + + /** + * Build a path from the two path components. Concatenate path1 and path2. Insert the file system-dependent + * 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 path resulting from concatenating path1 to path2 + */ + public String path(String path1, String path2); + + /** + * 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 + */ + public FileSystemEntry getEntry(String path); + + /** + * Return the parent path of the specified path. If path specifies a filename, + * then this method returns the path of the directory containing that file. If path + * specifies a directory, the this method returns its parent directory. If path is + * empty or does not have a parent component, then return an empty string. + * + *

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 path has no parent + * @throws AssertionError - if path is null + */ + public String getParent(String path); + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/FileSystemEntry.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/FileSystemEntry.java new file mode 100644 index 0000000..fefbee1 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/FileSystemEntry.java @@ -0,0 +1,98 @@ +/* + * 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.Date; + +/** + * Interface for an entry within a fake file system, representing a single file or directory. + * + * @author Chris Mair + */ +public interface FileSystemEntry { + + /** + * Return true if this entry represents a directory, false otherwise + * + * @return true if this file system entry is a directory, false otherwise + */ + public boolean isDirectory(); + + /** + * Return the path for this file system entry + * + * @return the path for this file system entry + */ + public String getPath(); + + /** + * Return the file name or directory name (no path) for this entry + * + * @return the file name or directory name (no path) for this entry + */ + public String getName(); + + /** + * Return the size of this file system entry + * + * @return the file size in bytes + */ + public long getSize(); + + /** + * Return the timestamp Date for the last modification of this file system entry + * + * @return the last modified timestamp Date for this file system entry + */ + public Date getLastModified(); + + /** + * Set the timestamp Date for the last modification of this file system entry + * + * @param lastModified - the lastModified value, as a Date + */ + public void setLastModified(Date lastModified); + + /** + * @return the username of the owner of this file system entry + */ + public String getOwner(); + + /** + * @return the name of the owning group for this file system entry + */ + public String getGroup(); + + /** + * @return the Permissions for this file system entry + */ + public 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); + + /** + * Lock down the path so it cannot be changed + */ + public void lockPath(); + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/FileSystemException.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/FileSystemException.java new file mode 100644 index 0000000..71b520b --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/FileSystemException.java @@ -0,0 +1,79 @@ +/* + * 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.MockFtpServerException; + +/** + * Represents an error that occurs while performing a FileSystem operation. + * + * @author Chris Mair + */ +public class FileSystemException extends MockFtpServerException { + + /** + * The path involved in the file system operation that caused the exception + */ + private String path; + + /** + * The message key for the exception message + */ + private String messageKey; + + /** + * Construct a new instance for the specified path and message key + * + * @param path - the path involved in the file system operation that caused the exception + * @param messageKey - the exception message key + */ + public FileSystemException(String path, String messageKey) { + super(path); + this.path = path; + this.messageKey = messageKey; + } + + /** + * @param path - the path involved in the file system operation that caused the exception + * @param messageKey - the exception message key + * @param cause - the exception cause, wrapped by this exception + */ + public FileSystemException(String path, String messageKey, Throwable cause) { + super(path, cause); + this.path = path; + this.messageKey = messageKey; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getMessageKey() { + return messageKey; + } + + public void setMessageKey(String messageKey) { + this.messageKey = messageKey; + } + + public String getMessage() { + return "FileSystemException[message=" + super.getMessage() + "; messageKey=" + getMessageKey() + "]"; + } +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/InvalidFilenameException.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/InvalidFilenameException.java new file mode 100644 index 0000000..3659c45 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/InvalidFilenameException.java @@ -0,0 +1,45 @@ +/* + * 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; + +/** + * Exception thrown when a path/filename is not valid. Causes include: + *

    + *
  • The filename contains invalid characters
  • + *
  • The path specifies a new filename, but its parent directory does not exist
  • + *
  • The path is expected to be a file, but actually specifies an existing directory
  • + *
+ */ +public class InvalidFilenameException extends FileSystemException { + + private static final String MESSAGE_KEY = "filesystem.pathIsNotValid"; + + /** + * @param path - the path involved in the file system operation that caused the exception + */ + public InvalidFilenameException(String path) { + super(path, MESSAGE_KEY); + } + + /** + * @param path - the path involved in the file system operation that caused the exception + * @param cause - the exception cause, wrapped by this exception + */ + public InvalidFilenameException(String path, Throwable cause) { + super(path, MESSAGE_KEY, cause); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/Permissions.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/Permissions.java new file mode 100644 index 0000000..c109407 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/Permissions.java @@ -0,0 +1,156 @@ +/* + * 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; + +/** + * Represents and encapsulates the read/write/execute permissions for a file or directory. + * This is conceptually (and somewhat loosely) based on the permissions flags within the Unix + * file system. An instance of this class is immutable. + * + * @author Chris Mair + */ +public class Permissions { + public static final Permissions ALL = new Permissions("rwxrwxrwx"); + public static final Permissions NONE = new Permissions("---------"); + public static final Permissions DEFAULT = ALL; + + private static final char READ_CHAR = 'r'; + private static final char WRITE_CHAR = 'w'; + private static final char EXECUTE_CHAR = 'x'; + + private String rwxString; + + /** + * Costruct a new instance for the specified read/write/execute specification String + * + * @param rwxString - the read/write/execute specification String; must be 9 characters long, with chars + * at index 0,3,6 == '-' or 'r', chars at index 1,4,7 == '-' or 'w' and chars at index 2,5,8 == '-' or 'x'. + */ + public Permissions(String rwxString) { + Assert.isTrue(rwxString.length() == 9, "The permissions string must be exactly 9 characters"); + final String RWX = "(-|r)(-|w)(-|x)"; + final String PATTERN = RWX + RWX + RWX; + Assert.isTrue(rwxString.matches(PATTERN), "The permissions string must match [" + PATTERN + "]"); + this.rwxString = rwxString; + } + + /** + * Return the read/write/execute specification String representing the set of permissions. For example: + * "rwxrwxrwx" or "rw-r-----". + * + * @return the String containing 9 characters that represent the read/write/execute permissions. + */ + public String asRwxString() { + return rwxString; + } + + /** + * @return the RWX string for this instance + */ + public String getRwxString() { + return rwxString; + } + + /** + * @see Object#equals(Object) + */ + public boolean equals(Object object) { + return (object != null) + && (object.getClass() == this.getClass()) + && (object.hashCode() == hashCode()); + } + + /** + * Return the hash code for this object. + * + * @see Object#hashCode() + */ + public int hashCode() { + return rwxString.hashCode(); + } + + /** + * @return true if and only if the user has read permission + */ + public boolean canUserRead() { + return rwxString.charAt(0) == READ_CHAR; + } + + /** + * @return true if and only if the user has write permission + */ + public boolean canUserWrite() { + return rwxString.charAt(1) == WRITE_CHAR; + } + + /** + * @return true if and only if the user has execute permission + */ + public boolean canUserExecute() { + return rwxString.charAt(2) == EXECUTE_CHAR; + } + + /** + * @return true if and only if the group has read permission + */ + public boolean canGroupRead() { + return rwxString.charAt(3) == READ_CHAR; + } + + /** + * @return true if and only if the group has write permission + */ + public boolean canGroupWrite() { + return rwxString.charAt(4) == WRITE_CHAR; + } + + /** + * @return true if and only if the group has execute permission + */ + public boolean canGroupExecute() { + return rwxString.charAt(5) == EXECUTE_CHAR; + } + + /** + * @return true if and only if the world has read permission + */ + public boolean canWorldRead() { + return rwxString.charAt(6) == READ_CHAR; + } + + /** + * @return true if and only if the world has write permission + */ + public boolean canWorldWrite() { + return rwxString.charAt(7) == WRITE_CHAR; + } + + /** + * @return true if and only if the world has execute permission + */ + public boolean canWorldExecute() { + return rwxString.charAt(8) == EXECUTE_CHAR; + } + + /** + * @return the String representation of this object. + */ + public String toString() { + return "Permissions[" + rwxString + "]"; + } +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/UnixDirectoryListingFormatter.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/UnixDirectoryListingFormatter.java new file mode 100644 index 0000000..ac7eb33 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/UnixDirectoryListingFormatter.java @@ -0,0 +1,89 @@ +/* + * 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.Logger; +import org.xbib.files.ftp.mock.core.util.StringUtil; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.Locale; + +/** + * Unix-specific implementation of the DirectoryListingFormatter interface. + * + * @author Chris Mair + */ +public class UnixDirectoryListingFormatter implements DirectoryListingFormatter { + + private static final Logger LOG = Logger.getLogger(UnixDirectoryListingFormatter.class.getName()); + + protected static final String DATE_FORMAT_YEAR = "MMM dd yyyy"; + protected static final String DATE_FORMAT_HOURS_MINUTES = "MMM dd HH:mm"; + private static final int SIZE_WIDTH = 15; + private static final int OWNER_WIDTH = 8; + private static final int GROUP_WIDTH = 8; + private static final String NONE = "none"; + + private Locale locale = Locale.ENGLISH; + + /** + * 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 + */ + public String format(FileSystemEntry fileSystemEntry) { + String dateStr = formatLastModifiedDate(fileSystemEntry); + String dirOrFile = fileSystemEntry.isDirectory() ? "d" : "-"; + String permissionsStr = formatPermissions(fileSystemEntry); + String linkCountStr = "1"; + String ownerStr = StringUtil.padRight(stringOrNone(fileSystemEntry.getOwner()), OWNER_WIDTH); + String groupStr = StringUtil.padRight(stringOrNone(fileSystemEntry.getGroup()), GROUP_WIDTH); + String sizeStr = StringUtil.padLeft(Long.toString(fileSystemEntry.getSize()), SIZE_WIDTH); + String listing = "" + dirOrFile + permissionsStr + " " + linkCountStr + " " + ownerStr + " " + groupStr + " " + sizeStr + " " + dateStr + " " + fileSystemEntry.getName(); + LOG.info("listing=[" + listing + "]"); + return listing; + } + + /** + * Set the Locale to be used in formatting the date within file/directory listings + * @param locale - the Locale instance + */ + public void setLocale(Locale locale) { + this.locale = locale; + } + + private String formatLastModifiedDate(FileSystemEntry fileSystemEntry) { + Date showYearThresholdDate = Date.from(Instant.now().minus(Duration.ofDays(180))); + String formatString = fileSystemEntry.getLastModified().before(showYearThresholdDate) ? DATE_FORMAT_YEAR : DATE_FORMAT_HOURS_MINUTES; + DateFormat dateFormat = new SimpleDateFormat(formatString, locale); + return dateFormat.format(fileSystemEntry.getLastModified()); + } + + private String formatPermissions(FileSystemEntry fileSystemEntry) { + Permissions permissions = fileSystemEntry.getPermissions() != null ? fileSystemEntry.getPermissions() : Permissions.DEFAULT; + return StringUtil.padRight(permissions.asRwxString(), 9); + } + + private String stringOrNone(String string) { + return (string == null) ? NONE : string; + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/UnixFakeFileSystem.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/UnixFakeFileSystem.java new file mode 100644 index 0000000..9dbca9c --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/UnixFakeFileSystem.java @@ -0,0 +1,88 @@ +/* + * 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; + +/** + * Implementation of the {@link FileSystem} interface that simulates a Unix + * file system. The rules for file and directory names include: + *
    + *
  • Filenames are case-sensitive
  • + *
  • Forward slashes (/) are the only valid path separators
  • + *
+ * + *

The directoryListingFormatter property is automatically initialized to an instance + * of {@link UnixDirectoryListingFormatter}. + * + * @author Chris Mair + */ +public class UnixFakeFileSystem extends AbstractFakeFileSystem { + + public static final String DEFAULT_SYSTEM_NAME = "UNIX"; + public static final char SEPARATOR = '/'; + + /** + * Construct a new instance and initialize the directoryListingFormatter to a UnixDirectoryListingFormatter. + */ + public UnixFakeFileSystem() { + this.setDirectoryListingFormatter(new UnixDirectoryListingFormatter()); + this.setSystemName(DEFAULT_SYSTEM_NAME); + } + + //------------------------------------------------------------------------- + // Abstract Method Implementations + //------------------------------------------------------------------------- + + protected char getSeparatorChar() { + return SEPARATOR; + } + + /** + * Return true if the specified path designates a valid (absolute) file path. For Unix, + * a path is valid if it starts with the '/' character, followed by zero or more names + * (a sequence of any characters except '/'), delimited by '/'. The path may optionally + * contain a terminating '/'. + * + * @param path - the path + * @return true if path is valid, false otherwise + * @throws AssertionError - if path is null + */ + protected boolean isValidName(String path) { + Assert.notNull(path, "path"); + // Any character but '/' + return path.matches("\\/|(\\/[^\\/]+\\/?)+"); + + } + + /** + * Return true if the specified char is a separator character ('\' or '/') + * + * @param c - the character to test + * @return true if the specified char is a separator character ('\' or '/') + */ + protected boolean isSeparator(char c) { + return c == SEPARATOR; + } + + /** + * @return true if the specified path component is a root for this filesystem + */ + protected boolean isRoot(String pathComponent) { + return pathComponent.indexOf(":") != -1; + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/WindowsDirectoryListingFormatter.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/WindowsDirectoryListingFormatter.java new file mode 100644 index 0000000..5e35063 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/WindowsDirectoryListingFormatter.java @@ -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.fake.filesystem; + +import org.xbib.files.ftp.mock.core.util.StringUtil; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Locale; + +/** + * Windows-specific implementation of the DirectoryListingFormatter interface. + * + * @author Chris Mair + */ +public class WindowsDirectoryListingFormatter implements DirectoryListingFormatter { + + private static final String DATE_FORMAT = "MM-dd-yy hh:mmaa"; + private static final int SIZE_WIDTH = 15; + + /** + * 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 + */ + public String format(FileSystemEntry fileSystemEntry) { + DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.ENGLISH); + String dateStr = dateFormat.format(fileSystemEntry.getLastModified()); + String dirOrSize = fileSystemEntry.isDirectory() + ? StringUtil.padRight("

", SIZE_WIDTH) + : StringUtil.padLeft(Long.toString(fileSystemEntry.getSize()), SIZE_WIDTH); + return dateStr + " " + dirOrSize + " " + fileSystemEntry.getName(); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/WindowsFakeFileSystem.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/WindowsFakeFileSystem.java new file mode 100644 index 0000000..68910cb --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/fake/filesystem/WindowsFakeFileSystem.java @@ -0,0 +1,103 @@ +/* + * 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; + +/** + * Implementation of the {@link FileSystem} interface that simulates a Microsoft + * Windows file system. The rules for file and directory names include: + *
    + *
  • Filenames are case-insensitive (and normalized to lower-case)
  • + *
  • Either forward slashes (/) or backward slashes (\) are valid path separators (but are normalized to '\')
  • + *
  • An absolute path starts with a drive specifier (e.g. 'a:' or 'c:') followed + * by '\' or '/', or else if it starts with "\\"
  • + *
+ * + *

The directoryListingFormatter property is automatically initialized to an instance + * of {@link WindowsDirectoryListingFormatter}. + * + * @author Chris Mair + */ +public class WindowsFakeFileSystem extends AbstractFakeFileSystem { + + public static final String DEFAULT_SYSTEM_NAME = "WINDOWS"; + public static final char SEPARATOR = '\\'; + private static final String VALID_PATTERN = "\\p{Alpha}\\:" + "(\\\\|(\\\\[^\\\\\\:\\*\\?\\<\\>\\|\\\"]+)+)"; + //static final VALID_PATTERN = /\p{Alpha}\:(\\|(\\[^\\\:\*\?\<\>\|\"]+)+)/ + private static final String LAN_PREFIX = "\\\\"; + + /** + * Construct a new instance and initialize the directoryListingFormatter to a WindowsDirectoryListingFormatter. + */ + public WindowsFakeFileSystem() { + this.setDirectoryListingFormatter(new WindowsDirectoryListingFormatter()); + this.setSystemName(DEFAULT_SYSTEM_NAME); + } + + //------------------------------------------------------------------------- + // Abstract Or Overridden Method Implementations + //------------------------------------------------------------------------- + + /** + * Return the normalized and unique key used to access the file system entry. Windows is case-insensitive, + * so normalize all paths to lower-case. + * + * @param path - the path + * @return the corresponding normalized key + */ + protected String getFileSystemEntryKey(String path) { + return normalize(path).toLowerCase(); + } + + protected char getSeparatorChar() { + return SEPARATOR; + } + + /** + * Return true if the specified path designates a valid (absolute) file path. For Windows + * paths, a path is valid if it starts with a drive specifier followed by + * '\' or '/', or if it starts with "\\". + * + * @param path - the path + * @return true if path is valid, false otherwise + * @throws AssertionError - if path is null + */ + protected boolean isValidName(String path) { + // \/:*?"<>| + Assert.notNull(path, "path"); + String standardized = path.replace('/', '\\'); + return standardized.matches(VALID_PATTERN) || standardized.startsWith(LAN_PREFIX); + } + + /** + * Return true if the specified char is a separator character ('\' or '/') + * + * @param c - the character to test + * @return true if the specified char is a separator character ('\' or '/') + */ + protected boolean isSeparator(char c) { + return c == '\\' || c == '/'; + } + + /** + * @return true if the specified path component is a root for this filesystem + */ + protected boolean isRoot(String pathComponent) { + return pathComponent.indexOf(":") != -1; + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/StubFtpServer.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/StubFtpServer.java new file mode 100644 index 0000000..88033d0 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/StubFtpServer.java @@ -0,0 +1,182 @@ +/* + * 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.stub; + +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; +import org.xbib.files.ftp.mock.core.server.AbstractFtpServer; +import org.xbib.files.ftp.mock.stub.command.AborCommandHandler; +import org.xbib.files.ftp.mock.stub.command.AcctCommandHandler; +import org.xbib.files.ftp.mock.stub.command.AlloCommandHandler; +import org.xbib.files.ftp.mock.stub.command.AppeCommandHandler; +import org.xbib.files.ftp.mock.stub.command.CdupCommandHandler; +import org.xbib.files.ftp.mock.stub.command.CwdCommandHandler; +import org.xbib.files.ftp.mock.stub.command.DeleCommandHandler; +import org.xbib.files.ftp.mock.stub.command.EprtCommandHandler; +import org.xbib.files.ftp.mock.stub.command.EpsvCommandHandler; +import org.xbib.files.ftp.mock.stub.command.HelpCommandHandler; +import org.xbib.files.ftp.mock.stub.command.ListCommandHandler; +import org.xbib.files.ftp.mock.stub.command.MkdCommandHandler; +import org.xbib.files.ftp.mock.stub.command.ModeCommandHandler; +import org.xbib.files.ftp.mock.stub.command.NlstCommandHandler; +import org.xbib.files.ftp.mock.stub.command.NoopCommandHandler; +import org.xbib.files.ftp.mock.stub.command.PassCommandHandler; +import org.xbib.files.ftp.mock.stub.command.PasvCommandHandler; +import org.xbib.files.ftp.mock.stub.command.PortCommandHandler; +import org.xbib.files.ftp.mock.stub.command.PwdCommandHandler; +import org.xbib.files.ftp.mock.stub.command.QuitCommandHandler; +import org.xbib.files.ftp.mock.stub.command.ReinCommandHandler; +import org.xbib.files.ftp.mock.stub.command.RestCommandHandler; +import org.xbib.files.ftp.mock.stub.command.RetrCommandHandler; +import org.xbib.files.ftp.mock.stub.command.RmdCommandHandler; +import org.xbib.files.ftp.mock.stub.command.RnfrCommandHandler; +import org.xbib.files.ftp.mock.stub.command.RntoCommandHandler; +import org.xbib.files.ftp.mock.stub.command.SiteCommandHandler; +import org.xbib.files.ftp.mock.stub.command.SmntCommandHandler; +import org.xbib.files.ftp.mock.stub.command.StatCommandHandler; +import org.xbib.files.ftp.mock.stub.command.StorCommandHandler; +import org.xbib.files.ftp.mock.stub.command.StouCommandHandler; +import org.xbib.files.ftp.mock.stub.command.StruCommandHandler; +import org.xbib.files.ftp.mock.stub.command.SystCommandHandler; +import org.xbib.files.ftp.mock.stub.command.TypeCommandHandler; +import org.xbib.files.ftp.mock.stub.command.UserCommandHandler; + +/** + * StubFtpServer is the top-level class for a "stub" implementation 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. + * + *

StubFtpServer works out of the box with default command handlers that return + * success reply codes and empty data (for retrieved files, directory listings, etc.). + * The command handler for any command can be easily configured to return custom data + * or reply codes. Or it can be replaced with a custom {@link CommandHandler} + * implementation. This allows simulation of a complete range of both success and + * failure scenarios. The command handlers can also be interrogated to verify command + * invocation data such as command parameters and timestamps. + * + *

StubFtpServer can be fully configured programmatically or within the + * Spring Framework or similar container. + * + *

Starting the StubFtpServer

+ * Here is how to start the StubFtpServer with the default configuration. + *

+ * StubFtpServer stubFtpServer = new StubFtpServer();
+ * stubFtpServer.start();
+ * 
+ * + *

FTP Server Control Port

+ * By default, StubFtpServer binds to the server control port of 21. You can use a different server control + * port by setting the serverControlPort property. If you specify a value of 0, + * then a free port number will be chosen automatically; call getServerControlPort() AFTER + * start() 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. + * + *

Retrieving Command Handlers

+ * 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. For example: + *

+ * PwdCommandHandler pwdCommandHandler = (PwdCommandHandler) stubFtpServer.getCommandHandler("PWD");
+ * 
+ * + *

Replacing Command Handlers

+ * You can replace 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. For example: + *

+ * PwdCommandHandler pwdCommandHandler = new PwdCommandHandler();
+ * pwdCommandHandler.setDirectory("some/dir");
+ * stubFtpServer.setCommandHandler("PWD", pwdCommandHandler);
+ * 
+ * You can also replace multiple command handlers at once by using the {@link #setCommandHandlers(java.util.Map)} + * method. That is especially useful when configuring the server through the Spring Framework. + * + *

FTP Command Reply Text ResourceBundle

+ * + *

The default text associated 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 StubFtpServer extends AbstractFtpServer { + + /** + * Create a new instance. Initialize the default command handlers and + * reply text ResourceBundle. + */ + public StubFtpServer() { + PwdCommandHandler pwdCommandHandler = new PwdCommandHandler(); + + // Initialize the default CommandHandler mappings + setCommandHandler(CommandNames.ABOR, new AborCommandHandler()); + setCommandHandler(CommandNames.ACCT, new AcctCommandHandler()); + setCommandHandler(CommandNames.ALLO, new AlloCommandHandler()); + setCommandHandler(CommandNames.APPE, new AppeCommandHandler()); + setCommandHandler(CommandNames.PWD, pwdCommandHandler); // same as XPWD + setCommandHandler(CommandNames.CONNECT, new ConnectCommandHandler()); + 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.NOOP, new NoopCommandHandler()); + setCommandHandler(CommandNames.NLST, new NlstCommandHandler()); + setCommandHandler(CommandNames.PASS, new PassCommandHandler()); + setCommandHandler(CommandNames.PASV, new PasvCommandHandler()); + setCommandHandler(CommandNames.PORT, new PortCommandHandler()); + setCommandHandler(CommandNames.RETR, new RetrCommandHandler()); + setCommandHandler(CommandNames.QUIT, new QuitCommandHandler()); + setCommandHandler(CommandNames.REIN, new ReinCommandHandler()); + setCommandHandler(CommandNames.REST, new RestCommandHandler()); + setCommandHandler(CommandNames.RMD, new RmdCommandHandler()); + setCommandHandler(CommandNames.RNFR, new RnfrCommandHandler()); + setCommandHandler(CommandNames.RNTO, new RntoCommandHandler()); + setCommandHandler(CommandNames.SITE, new SiteCommandHandler()); + 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.UNSUPPORTED, new UnsupportedCommandHandler()); + setCommandHandler(CommandNames.XPWD, pwdCommandHandler); // same as PWD + } + + //------------------------------------------------------------------------- + // Abstract method implementation + //------------------------------------------------------------------------- + + protected void initializeCommandHandler(CommandHandler commandHandler) { + ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, getReplyTextBundle()); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AborCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AborCommandHandler.java new file mode 100644 index 0000000..cf9fd52 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AborCommandHandler.java @@ -0,0 +1,44 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the ABOR command. Return a reply code of 226. + * + *

Each invocation record stored by this CommandHandler contains no data elements. + * + * @author Chris Mair + */ +public class AborCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + /** + * Constructor. Initialize the replyCode. + */ + public AborCommandHandler() { + setReplyCode(ReplyCodes.ABOR_OK); + } + + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AbstractStorCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AbstractStorCommandHandler.java new file mode 100644 index 0000000..799a174 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AbstractStorCommandHandler.java @@ -0,0 +1,49 @@ +/* + * 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.stub.command; + +import java.util.logging.Level; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.InvocationRecord; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * Abstract superclass for CommandHandler for commands that store a file. Send back two replies on the + * control connection: a reply code of 150 and another of 226. + * + *

Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #FILE_CONTENTS_KEY} ("fileContents") - the file contents (byte[]) sent on the data connection + *
+ * + * @author Chris Mair + */ +public abstract class AbstractStorCommandHandler extends AbstractStubDataCommandHandler { + + public static final String PATHNAME_KEY = "pathname"; + public static final String FILE_CONTENTS_KEY = "filecontents"; + + /** + * @see AbstractStubDataCommandHandler#processData(Command, Session, InvocationRecord) + */ + protected void processData(Command command, Session session, InvocationRecord invocationRecord) { + byte[] data = session.readData(); + LOG.log(Level.FINE, "Received " + data.length + " bytes"); + LOG.log(Level.FINE, "Received data [" + new String(data) + "]"); + invocationRecord.set(FILE_CONTENTS_KEY, data); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AbstractStubCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AbstractStubCommandHandler.java new file mode 100644 index 0000000..c2e82c5 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AbstractStubCommandHandler.java @@ -0,0 +1,31 @@ +/* + * 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.stub.command; + +import org.xbib.files.ftp.mock.core.command.AbstractStaticReplyCommandHandler; +import org.xbib.files.ftp.mock.stub.StubFtpServer; + +/** + * The abstract superclass for CommandHandler classes for the {@link StubFtpServer}. + * + *

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 AbstractStubCommandHandler extends AbstractStaticReplyCommandHandler { + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AbstractStubDataCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AbstractStubDataCommandHandler.java new file mode 100644 index 0000000..df9df4b --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AbstractStubDataCommandHandler.java @@ -0,0 +1,230 @@ +/* + * 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.stub.command; + +import org.xbib.files.ftp.mock.core.command.AbstractTrackingCommandHandler; +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.InvocationRecord; +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.AssertFailedException; + +/** + * Abstract superclass for CommandHandlers that read from or write to the data connection. + * + *

Return two replies on the control connection: by default a reply code of 150 before the + * data transfer across the data connection and another reply of 226 after the data transfer + * is complete. + * + *

This class implements the Template Method pattern. Subclasses must implement the abstract + * processData method to perform read or writes across the data connection. + * + *

Subclasses can optionally override the {@link #beforeProcessData(Command, Session, InvocationRecord)} + * method for logic before the data transfer or the {@link #afterProcessData(Command, Session, InvocationRecord)} + * method for logic after the data transfer. + * + *

Subclasses can optionally override the reply code and/or text for the initial reply (before + * the data transfer across the data connection) by calling {@link #setPreliminaryReplyCode(int)}, + * {@link #setPreliminaryReplyMessageKey(String)} and/or {@link #setPreliminaryReplyText(String)} + * methods. + * + *

Subclasses can optionally override the reply code and/or text for the final reply (after the + * the data transfer is complete) by calling {@link #setFinalReplyCode(int)}, + * {@link #setFinalReplyMessageKey(String)} and/or {@link #setFinalReplyText(String)} methods. + * + * @author Chris Mair + */ +public abstract class AbstractStubDataCommandHandler extends AbstractTrackingCommandHandler implements CommandHandler { + + // The completion reply code sent before the data transfer + protected int preliminaryReplyCode = 0; + + // The text for the preliminary reply. If null, use the default message associated with the reply code. + // If not null, this value overrides the preliminaryReplyMessageKey - i.e., this text is used instead of + // a localized message. + protected String preliminaryReplyText = null; + + // The message key for the preliminary reply text. If null, use the default message associated with + // the reply code. + protected String preliminaryReplyMessageKey = null; + + // The completion reply code sent after data transfer + protected int finalReplyCode = 0; + + // The text for the completion reply. If null, use the default message associated with the reply code. + // If not null, this value overrides the finalReplyMessageKey - i.e., this text is used instead of + // a localized message. + protected String finalReplyText = null; + + // The message key for the completion reply text. If null, use the default message associated with the reply code + protected String finalReplyMessageKey = null; + + /** + * Constructor. Initialize the preliminary and final reply code. + */ + protected AbstractStubDataCommandHandler() { + setPreliminaryReplyCode(ReplyCodes.TRANSFER_DATA_INITIAL_OK); + setFinalReplyCode(ReplyCodes.TRANSFER_DATA_FINAL_OK); + } + + /** + * Handle the command. Perform the following steps: + *

    + *
  1. Invoke the beforeProcessData() method
  2. + *
  3. Open the data connection
  4. + *
  5. Send an preliminary reply, default reply code 150
  6. + *
  7. Invoke the processData() method
  8. + *
  9. Close the data connection
  10. + *
  11. Send the final reply, default reply code 226
  12. + *
  13. Invoke the afterProcessData() method
  14. + *
+ * + * @see AbstractTrackingCommandHandler#handleCommand(Command, Session, InvocationRecord) + */ + public final void handleCommand(Command command, Session session, InvocationRecord invocationRecord) throws Exception { + + beforeProcessData(command, session, invocationRecord); + + sendPreliminaryReply(session); + session.openDataConnection(); + processData(command, session, invocationRecord); + session.closeDataConnection(); + sendFinalReply(session); + + afterProcessData(command, session, invocationRecord); + } + + /** + * Send the final reply. The default implementation sends a reply code of 226 with the + * corresponding associated reply text. + * + * @param session - the Session + */ + protected void sendFinalReply(Session session) { + sendReply(session, finalReplyCode, finalReplyMessageKey, finalReplyText, null); + } + + /** + * Perform any necessary logic before transferring data across the data connection. + * Do nothing by default. Subclasses should override to validate command parameters and + * store information in the InvocationRecord. + * + * @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 void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception { + // Do nothing by default + } + + /** + * Abstract method placeholder for subclass transfer of data across the data connection. + * Subclasses must override. The data connection is opened before this method and is + * closed after this method completes. + * + * @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 processData(Command command, Session session, InvocationRecord invocationRecord) throws Exception; + + /** + * Perform any necessary logic after transferring data across the data connection. + * Do nothing by default. + * + * @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 void afterProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception { + // Do nothing by default + } + + /** + * Send the preliminary reply for this command on the control connection. + * + * @param session - the Session + */ + private void sendPreliminaryReply(Session session) { + sendReply(session, preliminaryReplyCode, preliminaryReplyMessageKey, preliminaryReplyText, null); + } + + /** + * Set the completion reply code sent after data transfer + * + * @param finalReplyCode - the final reply code + * @throws AssertFailedException - if the finalReplyCode is invalid + */ + public void setFinalReplyCode(int finalReplyCode) { + assertValidReplyCode(finalReplyCode); + this.finalReplyCode = finalReplyCode; + } + + /** + * Set the message key for the completion reply text sent after data transfer + * + * @param finalReplyMessageKey - the final reply message key + */ + public void setFinalReplyMessageKey(String finalReplyMessageKey) { + this.finalReplyMessageKey = finalReplyMessageKey; + } + + /** + * Set the text of the completion reply sent after data transfer + * + * @param finalReplyText - the final reply text + */ + public void setFinalReplyText(String finalReplyText) { + this.finalReplyText = finalReplyText; + } + + /** + * Set the completion reply code sent before data transfer + * + * @param preliminaryReplyCode - the preliminary reply code to set + * @throws AssertFailedException - if the preliminaryReplyCode is invalid + */ + public void setPreliminaryReplyCode(int preliminaryReplyCode) { + assertValidReplyCode(preliminaryReplyCode); + this.preliminaryReplyCode = preliminaryReplyCode; + } + + /** + * Set the message key for the completion reply text sent before data transfer + * + * @param preliminaryReplyMessageKey - the preliminary reply message key + */ + public void setPreliminaryReplyMessageKey(String preliminaryReplyMessageKey) { + this.preliminaryReplyMessageKey = preliminaryReplyMessageKey; + } + + /** + * Set the text of the completion reply sent before data transfer + * + * @param preliminaryReplyText - the preliminary reply text + */ + public void setPreliminaryReplyText(String preliminaryReplyText) { + this.preliminaryReplyText = preliminaryReplyText; + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AcctCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AcctCommandHandler.java new file mode 100644 index 0000000..994d16d --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AcctCommandHandler.java @@ -0,0 +1,53 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the ACCT command. Send back a reply code of 230. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • "acount" - the account submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class AcctCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String ACCOUNT_KEY = "account"; + + /** + * Constructor. Initialize the replyCode. + */ + public AcctCommandHandler() { + setReplyCode(ReplyCodes.ACCT_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + invocationRecord.set(ACCOUNT_KEY, command.getRequiredParameter(0)); + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AlloCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AlloCommandHandler.java new file mode 100644 index 0000000..b886ada --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AlloCommandHandler.java @@ -0,0 +1,72 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +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.Assert; + +import java.util.StringTokenizer; + +/** + * CommandHandler for the ALLO (Allocate) command. Send back a reply code of 200. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #NUMBER_OF_BYTES_KEY} ("numberOfBytes") - the number of bytes submitted + * on the invocation (the first command parameter) + *
  • {@link #RECORD_SIZE_KEY} ("recordSize") - the record size optionally submitted + * on the invocation (the second command parameter) + *
+ * + * @author Chris Mair + */ +public class AlloCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String NUMBER_OF_BYTES_KEY = "numberOfBytes"; + public static final String RECORD_SIZE_KEY = "recordSize"; + private static final String RECORD_SIZE_DELIMITER = " R "; + + /** + * Constructor. Initialize the replyCode. + */ + public AlloCommandHandler() { + setReplyCode(ReplyCodes.ALLO_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + String parametersString = command.getRequiredParameter(0); + + if (parametersString.indexOf(RECORD_SIZE_DELIMITER) == -1) { + invocationRecord.set(NUMBER_OF_BYTES_KEY, Integer.valueOf(parametersString)); + } else { + // If the recordSize delimiter (" R ") is specified, then it must be followed by the recordSize. + StringTokenizer tokenizer = new StringTokenizer(parametersString, RECORD_SIZE_DELIMITER); + invocationRecord.set(NUMBER_OF_BYTES_KEY, Integer.valueOf(tokenizer.nextToken())); + Assert.isTrue(tokenizer.hasMoreTokens(), "Missing record size: [" + parametersString + "]"); + invocationRecord.set(RECORD_SIZE_KEY, Integer.valueOf(tokenizer.nextToken())); + } + + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AppeCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AppeCommandHandler.java new file mode 100644 index 0000000..cbfab61 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/AppeCommandHandler.java @@ -0,0 +1,44 @@ +/* + * 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.stub.command; + +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.InvocationRecord; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the APPE (Append) command. Send back two replies on the control connection: a + * reply code of 150 and another of 226. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PATHNAME_KEY} ("pathname") - the pathname of the directory submitted on the invocation (the first command parameter) + *
  • {@link #FILE_CONTENTS_KEY} ("fileContents") - the file contents (byte[]) sent on the data connection + *
+ * + * @author Chris Mair + */ +public class AppeCommandHandler extends AbstractStorCommandHandler { + + /** + * @see AbstractStubDataCommandHandler#beforeProcessData(Command, Session, InvocationRecord) + */ + protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception { + String filename = command.getRequiredParameter(0); + invocationRecord.set(PATHNAME_KEY, filename); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/CdupCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/CdupCommandHandler.java new file mode 100644 index 0000000..9b51c15 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/CdupCommandHandler.java @@ -0,0 +1,44 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the CDUP (Change To Parent Directory) command. Send back a reply code of 250. + *

+ * Each invocation record stored by this CommandHandler contains no data elements. + * + * @author Chris Mair + */ +public class CdupCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + /** + * Constructor. Initialize the replyCode. + */ + public CdupCommandHandler() { + setReplyCode(ReplyCodes.CDUP_OK); + } + + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/CwdCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/CwdCommandHandler.java new file mode 100644 index 0000000..1c8c5f3 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/CwdCommandHandler.java @@ -0,0 +1,53 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the CWD (Change Working Directory) command. Send back a reply code of 250. + * + *

Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PATHNAME_KEY} ("pathname") - the pathname of the directory submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class CwdCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String PATHNAME_KEY = "pathname"; + + /** + * Constructor. Initiate the replyCode. + */ + public CwdCommandHandler() { + setReplyCode(ReplyCodes.CWD_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + invocationRecord.set(PATHNAME_KEY, command.getRequiredParameter(0)); + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/DeleCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/DeleCommandHandler.java new file mode 100644 index 0000000..7be4180 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/DeleCommandHandler.java @@ -0,0 +1,53 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the DELE (Delete) command. Send back a reply code of 250. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PATHNAME_KEY} ("pathname") - the pathname of the file name submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class DeleCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String PATHNAME_KEY = "pathname"; + + /** + * Constructor. Initialize the replyCode. + */ + public DeleCommandHandler() { + setReplyCode(ReplyCodes.DELE_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + invocationRecord.set(PATHNAME_KEY, command.getRequiredParameter(0)); + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/EprtCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/EprtCommandHandler.java new file mode 100644 index 0000000..2ffe2ab --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/EprtCommandHandler.java @@ -0,0 +1,71 @@ +/* + * 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.stub.command; + +import java.util.logging.Level; +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.InvocationRecord; +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 org.xbib.files.ftp.mock.core.util.HostAndPort; + +import java.net.UnknownHostException; + +/** + * CommandHandler for the EPRT command. Send back a reply code of 200. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #HOST_KEY} ("host") - the client data host (InetAddress) submitted on the invocation (from parameters 1-4) + *
  • {@link #PORT_KEY} ("port") - the port number (Integer) submitted on the invocation (from parameter 5-6) + *
+ * See RFC2428 for more information. + * + * @author Chris Mair + */ +public class EprtCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String HOST_KEY = "host"; + public static final String PORT_KEY = "port"; + + /** + * Constructor. Initialize the replyCode. + */ + public EprtCommandHandler() { + setReplyCode(ReplyCodes.EPRT_OK); + } + + /** + * Handle the command + * + * @throws UnknownHostException - if the remote host is unknown + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) throws UnknownHostException { + 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); + invocationRecord.set(HOST_KEY, client.host); + invocationRecord.set(PORT_KEY, client.port); + sendReply(session); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/EpsvCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/EpsvCommandHandler.java new file mode 100644 index 0000000..34a3582 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/EpsvCommandHandler.java @@ -0,0 +1,61 @@ +/* + * 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.stub.command; + +import java.util.logging.Level; +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +import java.io.IOException; +import java.net.InetAddress; + +/** + * CommandHandler for the EPSV (Extended Address Passive Mode) command. Request the Session to switch + * to passive data connection mode. Return a reply code of 229, along with response text of the form: + * "Entering Extended Passive Mode (|||PORT|)", where PORT is the 16-bit TCP port + * address of the data connection on the server to which the client must connect. + * See RFC2428 for more information. + *

+ * Each invocation record stored by this CommandHandler contains no data elements. + * + * @author Chris Mair + */ +public class EpsvCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + /** + * Constructor. Initialize the replyCode. + */ + public EpsvCommandHandler() { + setReplyCode(ReplyCodes.EPSV_OK); + } + + /** + * @throws IOException - if an error occurs + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) + throws IOException { + + int port = session.switchToPassiveMode(); + InetAddress server = session.getServerHost(); + LOG.log(Level.FINE, "server=" + server + " port=" + port); + sendReply(session, Integer.toString(port)); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/FileRetrCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/FileRetrCommandHandler.java new file mode 100644 index 0000000..6e07593 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/FileRetrCommandHandler.java @@ -0,0 +1,111 @@ +/* + * 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.stub.command; + +import java.util.logging.Level; +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.InvocationRecord; +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.io.IOException; +import java.io.InputStream; + +/** + * CommandHandler for the RETR command. Returns the contents of the specified file on the + * data connection, along with two replies on the control connection: a reply code of 150 and + * another of 226. + * + *

The file property specifies the pathname for the file whose contents should + * be returned from this command. The file path is relative to the CLASSPATH (using the + * ClassLoader for this class). + * + *

An exception is thrown if the file property has not been set or if the specified + * file does not exist or cannot be read. + * + *

Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PATHNAME_KEY} ("pathname") - the pathname of the file submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class FileRetrCommandHandler extends AbstractStubDataCommandHandler implements CommandHandler { + + public static final String PATHNAME_KEY = "pathname"; + public static final int BUFFER_SIZE = 512; // package-private for testing + + private String file; + + /** + * Create new uninitialized instance + */ + public FileRetrCommandHandler() { + } + + /** + * Create new instance using the specified file pathname + * + * @param file - the path to the file + * @throws AssertFailedException - if the file is null + */ + public FileRetrCommandHandler(String file) { + setFile(file); + } + + /** + * @see AbstractStubDataCommandHandler#beforeProcessData(Command, Session, InvocationRecord) + */ + protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception { + Assert.notNull(file, "file"); + invocationRecord.set(PATHNAME_KEY, command.getRequiredParameter(0)); + } + + /** + * @see AbstractStubDataCommandHandler#processData(Command, Session, InvocationRecord) + */ + protected void processData(Command command, Session session, InvocationRecord invocationRecord) { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(file); + Assert.notNull(inputStream, "InputStream for [" + file + "]"); + byte[] buffer = new byte[BUFFER_SIZE]; + try { + int numBytes; + while ((numBytes = inputStream.read(buffer)) != -1) { + LOG.log(Level.FINEST, "Sending " + numBytes + " bytes..."); + session.sendData(buffer, numBytes); + } + } + catch (IOException e) { + throw new MockFtpServerException(e); + } + } + + /** + * Set the path of the file whose contents should be returned when this command is + * invoked. The path is relative to the CLASSPATH. + * + * @param file - the path to the file + * @throws AssertFailedException - if the file is null + */ + public void setFile(String file) { + Assert.notNull(file, "file"); + this.file = file; + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/HelpCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/HelpCommandHandler.java new file mode 100644 index 0000000..cc8c201 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/HelpCommandHandler.java @@ -0,0 +1,68 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +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.command.AbstractTrackingCommandHandler; + +/** + * CommandHandler for the HELP command. By default, return an empty help message, + * along with a reply code of 214. You can customize the returned help message by + * setting the helpMessage property. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #COMMAND_NAME_KEY} ("commandName") - the command name optionally submitted on + * the invocation (the first command parameter). May be null. + *
+ * + * @author Chris Mair + */ +public final class HelpCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String COMMAND_NAME_KEY = "commandName"; + + private String helpMessage = ""; + + /** + * Constructor. Initialize the replyCode. + */ + public HelpCommandHandler() { + setReplyCode(ReplyCodes.HELP_OK); + } + + /** + * @see AbstractTrackingCommandHandler#handleCommand(Command, Session, InvocationRecord) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + invocationRecord.set(COMMAND_NAME_KEY, command.getOptionalString(0)); + sendReply(session, helpMessage); + } + + /** + * Set the help message String to be returned by this command + * + * @param helpMessage - the help message + */ + public void setHelpMessage(String helpMessage) { + this.helpMessage = helpMessage; + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/ListCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/ListCommandHandler.java new file mode 100644 index 0000000..032f6f3 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/ListCommandHandler.java @@ -0,0 +1,90 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the LIST command. Return the configured directory listing on the data + * connection, along with two replies on the control connection: a reply code of 150 and + * another of 226. By default, return an empty directory listing. You can customize the + * returned directory listing by setting the directoryListing property. + *

+ * The interpretation of the value returned from this command is dependent upon the value returned + * by the SYST command. The format of the directory listing should match the format associated with + * the system named by the SYST command. For example, if the SYST command returns "WINDOWS", then + * the directory listing value from this command should match the Windows-specific format. See the + * SystCommandHandler to control the value returned for the SYST command. + *

+ * Here is an example value for directoryListing when the SystCommandHandler + * returns "WINDOWS". Note that multiple entries are separated by "\n": + *


+ *      CommandHandler listCommandHandler = new ListCommandHandler();
+ *      listCommandHandler.setDirectoryListing("11-09-01 12:30PM 406348 File2350.log\n" +
+ *          "11-01-01 1:30PM <DIR>  archive");
+ * 
+ *

+ * And here is an example value for directoryListing when the SystCommandHandler + * returns "UNIX". Note that multiple entries are separated by "\n": + *


+ *      CommandHandler listCommandHandler = new ListCommandHandler();
+ *      listCommandHandler.setDirectoryListing("drwxrwxrwx  1 none     none                   0 Mar 20  2010 archive\n" +
+ *          "-rwxrwxrwx  1 none     none                  19 Mar 20  2010 abc.txt");
+ * 
+ *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PATHNAME_KEY} ("pathname") - the pathname of the directory (or file) submitted on the + * invocation (the first command parameter); this parameter is optional, so the value may be null. + *
+ * + * @author Chris Mair + * @see SystCommandHandler + */ +public class ListCommandHandler extends AbstractStubDataCommandHandler implements CommandHandler { + + public static final String PATHNAME_KEY = "pathname"; + + private String directoryListing = ""; + + /** + * @see AbstractStubDataCommandHandler#beforeProcessData(Command, Session, InvocationRecord) + */ + protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception { + invocationRecord.set(PATHNAME_KEY, command.getOptionalString(0)); + } + + /** + * @see AbstractStubDataCommandHandler#processData(Command, Session, InvocationRecord) + */ + protected void processData(Command command, Session session, InvocationRecord invocationRecord) { + session.sendData(directoryListing.getBytes(), directoryListing.length()); + } + + /** + * Set the contents of the directoryListing to send back on the data connection for this command. + * The passed-in value is trimmed automatically. + * + * @param directoryListing - the directoryListing to set + */ + public void setDirectoryListing(String directoryListing) { + this.directoryListing = directoryListing.trim(); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/MkdCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/MkdCommandHandler.java new file mode 100644 index 0000000..8087729 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/MkdCommandHandler.java @@ -0,0 +1,54 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the MKD (Make Directory) command. Send back a reply code of 257. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PATHNAME_KEY} ("pathname") - the pathname of the directory submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class MkdCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String PATHNAME_KEY = "pathname"; + + /** + * Constructor. Initialize the replyCode. + */ + public MkdCommandHandler() { + setReplyCode(ReplyCodes.MKD_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + String pathname = command.getRequiredParameter(0); + invocationRecord.set(PATHNAME_KEY, pathname); + sendReply(session, pathname); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/ModeCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/ModeCommandHandler.java new file mode 100644 index 0000000..a9e73f8 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/ModeCommandHandler.java @@ -0,0 +1,53 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the MODE command. Send back a reply code of 200. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #MODE_KEY} ("mode") - the code for the transmission mode submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class ModeCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String MODE_KEY = "mode"; + + /** + * Constructor. Initialize the replyCode. + */ + public ModeCommandHandler() { + setReplyCode(ReplyCodes.MODE_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + invocationRecord.set(MODE_KEY, command.getRequiredParameter(0)); + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/NlstCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/NlstCommandHandler.java new file mode 100644 index 0000000..3a04391 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/NlstCommandHandler.java @@ -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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the NLST command. Return the configured directory listing on the data + * connection, along with two replies on the control connection: a reply code of 150 and + * another of 226. By default, return an empty directory listing. You can customize the + * returned directory listing by setting the directoryListing property. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PATHNAME_KEY} ("pathname") - the pathname of the directory (or file) submitted on the + * invocation (the first command parameter); this parameter is optional, so the value may be null. + *
+ * + * @author Chris Mair + */ +public class NlstCommandHandler extends AbstractStubDataCommandHandler implements CommandHandler { + + public static final String PATHNAME_KEY = "pathname"; + + private String directoryListing = ""; + + /** + * @see AbstractStubDataCommandHandler#beforeProcessData(Command, Session, InvocationRecord) + */ + protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception { + invocationRecord.set(PATHNAME_KEY, command.getOptionalString(0)); + } + + /** + * @see AbstractStubDataCommandHandler#processData(Command, Session, InvocationRecord) + */ + protected void processData(Command command, Session session, InvocationRecord invocationRecord) { + session.sendData(directoryListing.getBytes(), directoryListing.length()); + } + + /** + * Set the contents of the directoryListing to send back on the data connection for this command. + * The passed-in value is trimmed automatically. + * + * @param directoryListing - the directoryListing to set + */ + public void setDirectoryListing(String directoryListing) { + this.directoryListing = directoryListing.trim(); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/NoopCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/NoopCommandHandler.java new file mode 100644 index 0000000..0a1c273 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/NoopCommandHandler.java @@ -0,0 +1,44 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the NOOP command. Return a reply code of 200. + *

+ * Each invocation record stored by this CommandHandler contains no data elements. + * + * @author Chris Mair + */ +public class NoopCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + /** + * Constructor. Initialize the replyCode. + */ + public NoopCommandHandler() { + setReplyCode(ReplyCodes.NOOP_OK); + } + + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/PassCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/PassCommandHandler.java new file mode 100644 index 0000000..bddff5d --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/PassCommandHandler.java @@ -0,0 +1,53 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the PASS (Password) command. Send back a reply code of 230. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • "password" - the password submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class PassCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String PASSWORD_KEY = "password"; + + /** + * Constructor. Initialize the replyCode. + */ + public PassCommandHandler() { + setReplyCode(ReplyCodes.PASS_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + invocationRecord.set(PASSWORD_KEY, command.getRequiredParameter(0)); + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/PasvCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/PasvCommandHandler.java new file mode 100644 index 0000000..c772291 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/PasvCommandHandler.java @@ -0,0 +1,68 @@ +/* + * 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.stub.command; + +import java.util.logging.Level; +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.InvocationRecord; +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.Assert; +import org.xbib.files.ftp.mock.core.util.PortParser; + +import java.io.IOException; +import java.net.InetAddress; + +/** + * CommandHandler for the PASV (Passove Mode) command. Request the Session to switch to passive + * data connection mode. Return a reply code of 227, along with response text of the form: + * "Entering Passive Mode. (h1,h2,h3,h4,p1,p2)", where h1..h4 are the 4 + * bytes of the 32-bit internet host address of the server, and p1..p2 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. + *

+ * Each invocation record stored by this CommandHandler contains no data elements. + * + * @author Chris Mair + */ +public class PasvCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + /** + * Constructor. Initialize the replyCode. + */ + public PasvCommandHandler() { + setReplyCode(ReplyCodes.PASV_OK); + } + + /** + * @throws IOException - if an error occurs + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) + throws IOException { + + int port = session.switchToPassiveMode(); + InetAddress server = session.getServerHost(); + + Assert.isTrue(port > -1, "The server-side port is invalid: " + port); + LOG.log(Level.FINE, "server=" + server + " port=" + port); + String hostAndPort = "(" + PortParser.convertHostAndPortToCommaDelimitedBytes(server, port) + ")"; + + sendReply(session, hostAndPort); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/PortCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/PortCommandHandler.java new file mode 100644 index 0000000..b38ac62 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/PortCommandHandler.java @@ -0,0 +1,68 @@ +/* + * 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.stub.command; + +import java.util.logging.Level; +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.InvocationRecord; +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; + +import java.net.UnknownHostException; + +/** + * CommandHandler for the PORT command. Send back a reply code of 200. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #HOST_KEY} ("host") - the client data host (InetAddress) submitted on the invocation (from parameters 1-4) + *
  • {@link #PORT_KEY} ("port") - the port number (Integer) submitted on the invocation (from parameter 5-6) + *
+ * + * @author Chris Mair + */ +public class PortCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String HOST_KEY = "host"; + public static final String PORT_KEY = "port"; + + /** + * Constructor. Initialize the replyCode. + */ + public PortCommandHandler() { + setReplyCode(ReplyCodes.PORT_OK); + } + + /** + * Handle the command + * + * @throws UnknownHostException - if the remote host is not known + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) throws UnknownHostException { + HostAndPort client = PortParser.parseHostAndPort(command.getParameters()); + LOG.log(Level.FINE, "host=" + client.host + " port=" + client.port); + session.setClientDataHost(client.host); + session.setClientDataPort(client.port); + invocationRecord.set(HOST_KEY, client.host); + invocationRecord.set(PORT_KEY, client.port); + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/PwdCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/PwdCommandHandler.java new file mode 100644 index 0000000..d9fceea --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/PwdCommandHandler.java @@ -0,0 +1,58 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +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.command.AbstractCommandHandler; + +/** + * CommandHandler for the PWD (Print Working Directory) and XPWD commands. By default, return + * an empty directory name, along with a reply code of 257. You can customize the returned + * directory name by setting the directory property. + *

+ * Each invocation record stored by this CommandHandler contains no data elements. + * + * @author Chris Mair + */ +public class PwdCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + private String directory = ""; + + /** + * Constructor. Initialize the replyCode. + */ + public PwdCommandHandler() { + setReplyCode(ReplyCodes.PWD_OK); + } + + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + sendReply(session, AbstractCommandHandler.quotes(directory)); + } + + /** + * Set the directory String to be returned by this command + * + * @param directory - the directory + */ + public void setDirectory(String directory) { + this.directory = directory; + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/QuitCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/QuitCommandHandler.java new file mode 100644 index 0000000..f5e1a4b --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/QuitCommandHandler.java @@ -0,0 +1,45 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +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. + *

+ * Each invocation record stored by this CommandHandler contains no data elements. + * + * @author Chris Mair + */ +public class QuitCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + /** + * Constructor. Initialize the replyCode. + */ + public QuitCommandHandler() { + setReplyCode(ReplyCodes.QUIT_OK); + } + + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + sendReply(session); + session.close(); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/ReinCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/ReinCommandHandler.java new file mode 100644 index 0000000..a4235ba --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/ReinCommandHandler.java @@ -0,0 +1,45 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * /** + * CommandHandler for the REIN (Reinitialize) command. Send back a reply code of 220. + *

+ * Each invocation record stored by this CommandHandler contains no data elements. + * + * @author Chris Mair + */ +public class ReinCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + /** + * Constructor. Initialize the replyCode. + */ + public ReinCommandHandler() { + setReplyCode(ReplyCodes.REIN_OK); + } + + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RestCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RestCommandHandler.java new file mode 100644 index 0000000..381a1d1 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RestCommandHandler.java @@ -0,0 +1,54 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the REST (Restart of interrupted transfer) command. Send back a reply code of 350. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #MARKER_KEY} ("marker") - the server marker submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class RestCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String MARKER_KEY = "marker"; + + /** + * Constructor. Initialize the replyCode. + */ + public RestCommandHandler() { + setReplyCode(ReplyCodes.REST_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + String marker = command.getRequiredParameter(0); + invocationRecord.set(MARKER_KEY, marker); + sendReply(session, marker); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RetrCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RetrCommandHandler.java new file mode 100644 index 0000000..832a1ac --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RetrCommandHandler.java @@ -0,0 +1,113 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +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; + +/** + * CommandHandler for the RETR (Retrieve) command. Return the configured file contents on the data + * connection, along with two replies on the control connection: a reply code of 150 and + * another of 226. By default, return an empty file (i.e., a zero-length byte[]). You can + * customize the returned file contents by setting the fileContents property, + * specified either as a String or as a byte array. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PATHNAME_KEY} ("pathname") - the pathname of the file submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class RetrCommandHandler extends AbstractStubDataCommandHandler implements CommandHandler { + + public static final String PATHNAME_KEY = "pathname"; + + private byte[] fileContents = new byte[0]; + + /** + * Create new uninitialized instance + */ + public RetrCommandHandler() { + } + + /** + * Create new instance using the specified fileContents + * + * @param fileContents - the file contents + * @throws AssertFailedException + * - if the fileContents is null + */ + public RetrCommandHandler(String fileContents) { + setFileContents(fileContents); + } + + /** + * Create new instance using the specified fileContents + * + * @param fileContents - the file contents + * @throws AssertFailedException + * - if the fileContents is null + */ + public RetrCommandHandler(byte[] fileContents) { + setFileContents(fileContents); + } + + /** + * @see AbstractStubDataCommandHandler#beforeProcessData(Command, Session, InvocationRecord) + */ + protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception { + String filename = command.getRequiredParameter(0); + invocationRecord.set(PATHNAME_KEY, filename); + } + + /** + * @see AbstractStubDataCommandHandler#processData(Command, Session, InvocationRecord) + */ + protected void processData(Command command, Session session, InvocationRecord invocationRecord) { + LOG.info("Sending " + fileContents.length + " bytes"); + session.sendData(fileContents, fileContents.length); + } + + /** + * Set the file contents to return from subsequent command invocations + * + * @param fileContents - the fileContents to set + * @throws AssertFailedException + * - if the fileContents is null + */ + public void setFileContents(String fileContents) { + Assert.notNull(fileContents, "fileContents"); + setFileContents(fileContents.getBytes()); + } + + /** + * Set the file contents to return from subsequent command invocations + * + * @param fileContents - the file contents + * @throws AssertFailedException + * - if the fileContents is null + */ + public void setFileContents(byte[] fileContents) { + Assert.notNull(fileContents, "fileContents"); + this.fileContents = fileContents; + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RmdCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RmdCommandHandler.java new file mode 100644 index 0000000..95bb233 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RmdCommandHandler.java @@ -0,0 +1,53 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the RMD (Remove Working Directory) command. Send back a reply code of 250. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PATHNAME_KEY} ("pathname") - the pathname of the directory submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class RmdCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String PATHNAME_KEY = "pathname"; + + /** + * Constructor. Initialize the replyCode. + */ + public RmdCommandHandler() { + setReplyCode(ReplyCodes.RMD_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + invocationRecord.set(PATHNAME_KEY, command.getRequiredParameter(0)); + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RnfrCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RnfrCommandHandler.java new file mode 100644 index 0000000..3fdea82 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RnfrCommandHandler.java @@ -0,0 +1,53 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the RNFR (Rename From) command. Send back a reply code of 350. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PATHNAME_KEY} ("pathname") - the pathname of the file submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class RnfrCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String PATHNAME_KEY = "pathname"; + + /** + * Constructor. Initialize the replyCode. + */ + public RnfrCommandHandler() { + setReplyCode(ReplyCodes.RNFR_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + invocationRecord.set(PATHNAME_KEY, command.getRequiredParameter(0)); + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RntoCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RntoCommandHandler.java new file mode 100644 index 0000000..66b495a --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/RntoCommandHandler.java @@ -0,0 +1,53 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the RNTO (Rename To) command. Send back a reply code of 250. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PATHNAME_KEY} ("pathname") - the pathname of the file submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class RntoCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String PATHNAME_KEY = "pathname"; + + /** + * Constructor. Initialize the replyCode. + */ + public RntoCommandHandler() { + setReplyCode(ReplyCodes.RNTO_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + invocationRecord.set(PATHNAME_KEY, command.getRequiredParameter(0)); + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/SiteCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/SiteCommandHandler.java new file mode 100644 index 0000000..b54a258 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/SiteCommandHandler.java @@ -0,0 +1,53 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the SITE (Site Parameters) command. Send back a reply code of 200. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PARAMETERS_KEY} ("parameters") - the site parameters submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class SiteCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String PARAMETERS_KEY = "parameters"; + + /** + * Constructor. Initiate the replyCode. + */ + public SiteCommandHandler() { + setReplyCode(ReplyCodes.SITE_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + invocationRecord.set(PARAMETERS_KEY, command.getRequiredParameter(0)); + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/SmntCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/SmntCommandHandler.java new file mode 100644 index 0000000..6d9dbe2 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/SmntCommandHandler.java @@ -0,0 +1,53 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the SMNT (Structure Mount) command. Send back a reply code of 250. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PATHNAME_KEY} ("pathname") - the pathname of the directory submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class SmntCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String PATHNAME_KEY = "pathname"; + + /** + * Constructor. Initiate the replyCode. + */ + public SmntCommandHandler() { + setReplyCode(ReplyCodes.SMNT_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + invocationRecord.set(PATHNAME_KEY, command.getRequiredParameter(0)); + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/StatCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/StatCommandHandler.java new file mode 100644 index 0000000..89851e8 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/StatCommandHandler.java @@ -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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the STAT (Status) command. By default, return empty status information, + * along with a reply code of 211 if no pathname parameter is specified or 213 if a + * pathname is specified. You can customize the returned status information by setting + * the status property. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PATHNAME_KEY} ("pathname") - the pathname of the directory (or file) submitted on the + * invocation (the first command parameter); this parameter is optional, so the value may be null. + *
+ * + * @author Chris Mair + * @see SystCommandHandler + */ +public class StatCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String PATHNAME_KEY = "pathname"; + + private String status = ""; + + /** + * Constructor. + */ + public StatCommandHandler() { + // Do not initialize replyCode -- will be set dynamically + } + + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + String pathname = command.getOptionalString(0); + invocationRecord.set(PATHNAME_KEY, pathname); + + // Only use dynamic reply code if the replyCode property was NOT explicitly set + if (replyCode == 0) { + int code = (pathname == null) ? ReplyCodes.STAT_SYSTEM_OK : ReplyCodes.STAT_FILE_OK; + sendReply(session, code, replyMessageKey, replyText, new String[]{status}); + } else { + sendReply(session, status); + } + } + + /** + * Set the contents of the status to send back as the reply text for this command + * + * @param status - the status + */ + public void setStatus(String status) { + this.status = status; + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/StorCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/StorCommandHandler.java new file mode 100644 index 0000000..fc39588 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/StorCommandHandler.java @@ -0,0 +1,44 @@ +/* + * 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.stub.command; + +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.InvocationRecord; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the STOR (Store) command. Send back two replies on the control connection: a + * reply code of 150 and another of 226. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #PATHNAME_KEY} ("pathname") - the pathname of the directory submitted on the invocation (the first command parameter) + *
  • {@link #FILE_CONTENTS_KEY} ("fileContents") - the file contents (byte[]) sent on the data connection + *
+ * + * @author Chris Mair + */ +public class StorCommandHandler extends AbstractStorCommandHandler { + + /** + * @see AbstractStubDataCommandHandler#beforeProcessData(Command, Session, InvocationRecord) + */ + protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception { + String filename = command.getRequiredParameter(0); + invocationRecord.set(PATHNAME_KEY, filename); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/StouCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/StouCommandHandler.java new file mode 100644 index 0000000..0292555 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/StouCommandHandler.java @@ -0,0 +1,59 @@ +/* + * 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.stub.command; + +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the STOU (Store Unique) command. Send back two replies on the control connection: a + * reply code of 150 and another of 226. The text accompanying the final reply (226) is the + * unique filename, which is "" by default. You can customize the returned filename by setting + * the filename property. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #FILE_CONTENTS_KEY} ("fileContents") - the file contents (byte[]) sent on the data connection + *
+ * + * @author Chris Mair + */ +public class StouCommandHandler extends AbstractStorCommandHandler { + + private static final String FINAL_REPLY_TEXT_KEY = "226.WithFilename"; + + private String filename = ""; + + /** + * Override the default implementation to send a custom reply text that includes the STOU response filename + * + * @see AbstractStubDataCommandHandler#sendFinalReply(Session) + */ + protected void sendFinalReply(Session session) { + final String[] ARGS = {filename}; + sendReply(session, ReplyCodes.TRANSFER_DATA_FINAL_OK, FINAL_REPLY_TEXT_KEY, null, ARGS); + } + + /** + * Set the filename returned with the final reply of the STOU command + * + * @param filename - the filename + */ + public void setFilename(String filename) { + this.filename = filename; + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/StruCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/StruCommandHandler.java new file mode 100644 index 0000000..27695b3 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/StruCommandHandler.java @@ -0,0 +1,53 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the STRU (File Structure) command. Send back a reply code of 200. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #FILE_STRUCTURE_KEY} ("fileStructure") - the file structure code submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class StruCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String FILE_STRUCTURE_KEY = "fileStructure"; + + /** + * Constructor. Initialize the replyCode. + */ + public StruCommandHandler() { + setReplyCode(ReplyCodes.STRU_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + invocationRecord.set(FILE_STRUCTURE_KEY, command.getRequiredParameter(0)); + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/SystCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/SystCommandHandler.java new file mode 100644 index 0000000..28f64d9 --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/SystCommandHandler.java @@ -0,0 +1,62 @@ +/* + * 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.stub.command; + +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.InvocationRecord; +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.Assert; + +/** + * CommandHandler for the SYST (System) command. Send back a reply code of 215. By default, + * return "WINDOWS" as the system name. You can customize the returned name by + * setting the systemName property. + *

+ * See the available system names listed in the Assigned Numbers document + * (RFC 943). + *

+ * Each invocation record stored by this CommandHandler contains no data elements. + * + * @author Chris Mair + */ +public class SystCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + private String systemName = "WINDOWS"; + + /** + * Constructor. Initialize the replyCode. + */ + public SystCommandHandler() { + setReplyCode(ReplyCodes.SYST_OK); + } + + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + sendReply(session, quotes(systemName)); + } + + /** + * Set the systemName String to be returned by this command + * + * @param systemName - the systemName + */ + public void setSystemName(String systemName) { + Assert.notNull(systemName, "systemName"); + this.systemName = systemName; + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/TypeCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/TypeCommandHandler.java new file mode 100644 index 0000000..25ce3fe --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/TypeCommandHandler.java @@ -0,0 +1,58 @@ +/* + * 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.stub.command; + +import java.util.logging.Level; +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the TYPE command. Send back a reply code of 200. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #TYPE_INFO_KEY} ("typeInfo") - the type information submitted on the + * invocation, which is a String[2] containing the first two command parameter values. + *
+ * + * @author Chris Mair + */ +public class TypeCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String TYPE_INFO_KEY = "typeInfo"; + + /** + * Constructor. Initialize the replyCode. + */ + public TypeCommandHandler() { + setReplyCode(ReplyCodes.TYPE_OK); + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + LOG.log(Level.FINE, "Processing TYPE: " + command); + String type = command.getRequiredParameter(0); + String format = command.getOptionalString(1); + invocationRecord.set(TYPE_INFO_KEY, new String[]{type, format}); + sendReply(session); + } + +} diff --git a/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/UserCommandHandler.java b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/UserCommandHandler.java new file mode 100644 index 0000000..7d85ffb --- /dev/null +++ b/files-ftp-mock/src/main/java/org/xbib/files/ftp/mock/stub/command/UserCommandHandler.java @@ -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.stub.command; + +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; + +/** + * CommandHandler for the USER command. The passwordRequired property defaults to true, + * indicating that a password is required following the user name. If true, this command handler + * returns a reply of 331. If false, return a reply of 230. + *

+ * Each invocation record stored by this CommandHandler includes the following data element key/values: + *

    + *
  • {@link #USERNAME_KEY} ("username") - the user name submitted on the invocation (the first command parameter) + *
+ * + * @author Chris Mair + */ +public class UserCommandHandler extends AbstractStubCommandHandler implements CommandHandler { + + public static final String USERNAME_KEY = "username"; + + private boolean passwordRequired = true; + + /** + * Constructor. + */ + public UserCommandHandler() { + // Do not initialize replyCode -- will be set dynamically + } + + /** + * @see CommandHandler#handleCommand(Command, Session) + */ + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + invocationRecord.set(USERNAME_KEY, command.getRequiredParameter(0)); + + // Only use dynamic reply code if the replyCode property was NOT explicitly set + if (replyCode == 0) { + int code = (passwordRequired) ? ReplyCodes.USER_NEED_PASSWORD_OK : ReplyCodes.USER_LOGGED_IN_OK; + sendReply(session, code, replyMessageKey, replyText, null); + } else { + sendReply(session); + } + } + + /** + * Return true if a password is required at login. See {@link #setPasswordRequired(boolean)}. + * + * @return the passwordRequired flag + */ + public boolean isPasswordRequired() { + return passwordRequired; + } + + /** + * Set true to indicate that a password is required. If true, this command handler returns a reply + * of 331. If false, return a reply of 230. + * + * @param passwordRequired - is a password required for login + */ + public void setPasswordRequired(boolean passwordRequired) { + this.passwordRequired = passwordRequired; + } + +} diff --git a/files-ftp-mock/src/test/java/module-info.java b/files-ftp-mock/src/test/java/module-info.java new file mode 100644 index 0000000..a731cbb --- /dev/null +++ b/files-ftp-mock/src/test/java/module-info.java @@ -0,0 +1,10 @@ +module org.xbib.files.ftp.mock.test { + requires java.logging; + requires org.junit.jupiter.api; + requires org.junit.jupiter.params; + requires org.xbib.files.ftp.mock; + requires org.mockito; + requires org.xbib.files.ftp; + exports org.xbib.files.ftp.mock.test; + opens org.xbib.files.ftp.mock.test to org.junit.platform.commons; +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/AbstractTestCase.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/AbstractTestCase.java new file mode 100644 index 0000000..cfae3da --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/AbstractTestCase.java @@ -0,0 +1,115 @@ +/* + * 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.test; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.xbib.files.ftp.mock.core.MockFtpServerException; + +/** + * Abstract superclass for all project test classes + * + * @author Chris Mair + */ +public abstract class AbstractTestCase { + + protected static final String[] EMPTY = new String[0]; + protected static final InetAddress DEFAULT_HOST = inetAddress(null); + + //------------------------------------------------------------------------- + // Helper Methods + //------------------------------------------------------------------------- + + protected void log(Object message) { + System.out.println("[" + getClass().getSimpleName() + "]: " + message); + } + + /** + * Create and return a one-element Object[] containing the specified Object + * + * @param o - the object + * @return the Object array, of length 1, containing o + */ + protected static Object[] objArray(Object o) { + return new Object[]{o}; + } + + /** + * Create and return a one-element String[] containing the specified String + * + * @param s - the String + * @return the String array, of length 1, containing s + */ + protected static String[] array(String s) { + return new String[]{s}; + } + + /** + * Create and return a two-element String[] containing the specified Strings + * + * @param s1 - the first String + * @param s2 - the second String + * @return the String array, of length 2, containing s1 and s2 + */ + protected static String[] array(String s1, String s2) { + return new String[]{s1, s2}; + } + + /** + * Create a new InetAddress from the specified host String, using the + * {@link InetAddress#getByName(String)} method, wrapping any checked + * exception within a unchecked MockFtpServerException. + * + * @param host + * @return an InetAddress for the specified host + * @throws MockFtpServerException - if an UnknownHostException is thrown + */ + protected static InetAddress inetAddress(String host) { + try { + return InetAddress.getByName(host); + } + catch (UnknownHostException e) { + throw new MockFtpServerException(e); + } + } + + /** + * Create and return a List containing the single Object passed as an argument to this method + * + * @param element- the element to add + * @return the List containing the specified element + */ + protected static List list(Object element) { + return Collections.singletonList(element); + } + + /** + * Create and return a Set containing the Objects passed as arguments to this method + */ + protected static Set set(Object e1, Object... otherElements) { + Set set = new HashSet(); + set.add(e1); + for (Object element : otherElements) { + set.add(element); + } + return set; + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/IntegrationTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/IntegrationTest.java new file mode 100644 index 0000000..2253ec8 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/IntegrationTest.java @@ -0,0 +1,25 @@ +/* + * 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.test; + +/** + * Marker interface for integration test + * + * @author Chris Mair + */ +public interface IntegrationTest { + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/PortTestUtil.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/PortTestUtil.java new file mode 100644 index 0000000..f342180 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/PortTestUtil.java @@ -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.test; + +/** + * Contains static test utility method to determine FTP server port number to use for tests + * + * @author Chris Mair + */ +public final class PortTestUtil { + + private static final int DEFAULT_SERVER_CONTROL_PORT = 62999; // If this ends up causing collisions, then determine dynamically + private static final String FTP_SERVER_PORT_PROPERTY = "ftp.server.port"; + + /** + * Return the port number to use for the FTP server control port. If the "ftp.server.port" + * system property is defined, then use that value (converted to an integer), otherwise + * return the default port number of 21. + * + * @return the port number to use for the FTP server control port + */ + public static int getFtpServerControlPort() { + String systemProperty = System.getProperty(FTP_SERVER_PORT_PROPERTY); + return (systemProperty == null) ? DEFAULT_SERVER_CONTROL_PORT : Integer.parseInt(systemProperty); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/AbstractCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/AbstractCommandHandlerTest.java new file mode 100644 index 0000000..62b09e1 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/AbstractCommandHandlerTest.java @@ -0,0 +1,96 @@ +/* + * 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.test.core.command; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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.session.Session; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.test.AbstractTestCase; +import org.xbib.files.ftp.mock.stub.command.AbstractStubCommandHandler; + +import java.util.ListResourceBundle; +import java.util.ResourceBundle; + +/** + * Tests for the AbstractCommandHandler class. + * + * @author Chris Mair + */ +class AbstractCommandHandlerTest extends AbstractTestCase { + + private static final int REPLY_CODE1 = 777; + private static final int REPLY_CODE2 = 888; + private static final String REPLY_TEXT1 = "reply1 ... abcdef"; + private static final String REPLY_TEXT2 = "abc {0} def"; + private static final String MESSAGE_KEY = "key.123"; + private static final String MESSAGE_TEXT = "message.123"; + + private AbstractCommandHandler commandHandler; + + @Test + void testQuotes() { + assertEquals("\"abc\"", AbstractStubCommandHandler.quotes("abc")); + assertEquals("\"\"", AbstractStubCommandHandler.quotes("")); + } + + @Test + void testQuotes_Null() { + assertThrows(AssertFailedException.class, () -> AbstractStubCommandHandler.quotes(null)); + } + + @Test + void testAssertValidReplyCode() { + // These are valid, so expect no exceptions + commandHandler.assertValidReplyCode(1); + commandHandler.assertValidReplyCode(100); + + // These are invalid + testAssertValidReplyCodeWithInvalid(0); + testAssertValidReplyCodeWithInvalid(-1); + } + + private void testAssertValidReplyCodeWithInvalid(int invalidReplyCode) { + assertThrows(AssertFailedException.class, () -> commandHandler.assertValidReplyCode(invalidReplyCode)); + } + + //------------------------------------------------------------------------- + // Test setup + //------------------------------------------------------------------------- + + @BeforeEach + void setUp() { + commandHandler = new AbstractCommandHandler() { + public void handleCommand(Command command, Session session) { + } + }; + ResourceBundle replyTextBundle = new ListResourceBundle() { + protected Object[][] getContents() { + return new Object[][]{ + {Integer.toString(REPLY_CODE1), REPLY_TEXT1}, + {Integer.toString(REPLY_CODE2), REPLY_TEXT2}, + {MESSAGE_KEY, MESSAGE_TEXT} + }; + } + }; + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/AbstractCommandHandlerTestCase.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/AbstractCommandHandlerTestCase.java new file mode 100644 index 0000000..6fd852e --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/AbstractCommandHandlerTestCase.java @@ -0,0 +1,221 @@ +/* + * 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.test.core.command; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.xbib.files.ftp.mock.core.command.AbstractTrackingCommandHandler; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.InvocationHistory; +import org.xbib.files.ftp.mock.core.command.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.session.Session; +import org.xbib.files.ftp.mock.test.AbstractTestCase; + +import java.text.MessageFormat; +import java.util.ListResourceBundle; +import java.util.ResourceBundle; + +/** + * Abstract superclass for CommandHandler tests + * + * @author Chris Mair + */ +public abstract class AbstractCommandHandlerTestCase extends AbstractTestCase { + + // Some common test constants + protected static final String DIR1 = "dir1"; + protected static final String DIR2 = "dir2"; + protected static final String FILENAME1 = "sample1.txt"; + protected static final String FILENAME2 = "sample2.txt"; + + protected Session session; + protected ResourceBundle replyTextBundle; + + /** + * Test the handleCommand() method, when one or more parameter is missing or invalid + * + * @param commandHandler - the CommandHandler to test + * @param commandName - the name for the Command + * @param parameters - the Command parameters + */ + protected void testHandleCommand_InvalidParameters(AbstractTrackingCommandHandler commandHandler, + String commandName, String[] parameters) throws Exception { + Command command = new Command(commandName, parameters); + + commandHandler.handleCommand(command, session); + verify(session).sendReply(ReplyCodes.COMMAND_SYNTAX_ERROR, replyTextFor(ReplyCodes.COMMAND_SYNTAX_ERROR)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyNoDataElements(commandHandler.getInvocation(0)); + } + + /** + * Verify that the CommandHandler contains the specified number of invocation records + * + * @param commandHandler - the CommandHandler + * @param expected - the expected number of invocations + */ + protected void verifyNumberOfInvocations(InvocationHistory commandHandler, int expected) { + assertEquals(expected, commandHandler.numberOfInvocations()); + } + + /** + * Verify that the InvocationRecord contains no data elements + * + * @param invocationRecord - the InvocationRecord + */ + protected void verifyNoDataElements(InvocationRecord invocationRecord) { + log("Verifying: " + invocationRecord); + assertEquals(0, invocationRecord.keySet().size(), "number of data elements"); + } + + /** + * Verify that the InvocationRecord contains exactly one data element, with the specified key + * and value. + * + * @param invocationRecord - the InvocationRecord + * @param key - the expected key + * @param value - the expected value + */ + protected void verifyOneDataElement(InvocationRecord invocationRecord, String key, Object value) { + log("Verifying: " + invocationRecord); + assertEquals(1, invocationRecord.keySet().size(), "number of data elements"); + assertEqualsAllTypes("value:" + value, value, invocationRecord.getObject(key)); + } + + /** + * Verify that the InvocationRecord contains exactly two data element, with the specified keys + * and values. + * + * @param invocationRecord - the InvocationRecord + * @param key1 - the expected key1 + * @param value1 - the expected value1 + * @param key2 - the expected key2 + * @param value2- the expected value2 + */ + protected void verifyTwoDataElements(InvocationRecord invocationRecord, String key1, Object value1, String key2, Object value2) { + log("Verifying: " + invocationRecord); + assertEquals(2, invocationRecord.keySet().size(), "number of data elements"); + assertEqualsAllTypes("value1:" + value1, value1, invocationRecord.getObject(key1)); + assertEqualsAllTypes("value2:" + value2, value2, invocationRecord.getObject(key2)); + } + + /** + * Assert that the actual is equal to the expected, using arrays equality comparison if + * necessary + * + * @param message - the message, used if the comparison fails + * @param expected - the expected value + * @param actual - the actual value + */ + private void assertEqualsAllTypes(String message, Object expected, Object actual) { + if (expected instanceof byte[] || actual instanceof byte[]) { + assertArrayEquals((byte[]) expected, (byte[]) actual, message); + } else if (expected instanceof Object[] || actual instanceof Object[]) { + assertArrayEquals((Object[]) expected, (Object[]) actual, message); + } else { + assertEquals(expected, actual, message); + } + } + + @BeforeEach + void setUp_AbstractCommandHandlerTestCase() { + session = mock(Session.class); + when(session.getClientHost()).thenReturn(DEFAULT_HOST); + + replyTextBundle = new ListResourceBundle() { + protected Object[][] getContents() { + return new Object[][]{ + {"150", replyTextFor(150)}, + {"200", replyTextFor(200)}, + {"211", replyTextWithParameterFor(211)}, + {"213", replyTextWithParameterFor(213)}, + {"214", replyTextWithParameterFor(214)}, + {"215", replyTextWithParameterFor(215)}, + {"220", replyTextFor(220)}, + {"221", replyTextFor(221)}, + {"226", replyTextFor(226)}, + {"226.WithFilename", replyTextWithParameterFor("226.WithFilename")}, + {"227", replyTextWithParameterFor(227)}, + {"229", replyTextWithParameterFor(229)}, + {"230", replyTextFor(230)}, + {"250", replyTextFor(250)}, + {"257", replyTextWithParameterFor(257)}, + {"331", replyTextFor(331)}, + {"350", replyTextFor(350)}, + {"501", replyTextFor(501)}, + {"502", replyTextFor(502)}, + }; + } + }; + } + + /** + * Return the test-specific reply text for the specified reply code + * + * @param replyCode - the reply code + * @return the reply text for the specified reply code + */ + protected String replyTextFor(int replyCode) { + return "Reply for " + replyCode; + } + + /** + * Return the test-specific parameterized reply text for the specified reply code + * + * @param replyCode - the reply code + * @return the reply text for the specified reply code + */ + protected String replyTextWithParameterFor(int replyCode) { + return "Reply for " + replyCode + ":{0}"; + } + + /** + * Return the test-specific parameterized reply text for the specified messageKey + * + * @param messageKey - the messageKey + * @return the reply text for the specified messageKey + */ + protected String replyTextWithParameterFor(String messageKey) { + return "Reply for " + messageKey + ":{0}"; + } + + /** + * Return the test-specific reply text for the specified reply code and message parameter + * + * @param replyCode - the reply code + * @param parameter - the message parameter value + * @return the reply text for the specified reply code + */ + protected String formattedReplyTextFor(int replyCode, Object parameter) { + return MessageFormat.format(replyTextWithParameterFor(replyCode), objArray(parameter)); + } + + /** + * Return the test-specific reply text for the specified message key and message parameter + * + * @param messageKey - the messageKey + * @param parameter - the message parameter value + * @return the reply text for the specified message key and parameter + */ + protected String formattedReplyTextFor(String messageKey, Object parameter) { + return MessageFormat.format(replyTextWithParameterFor(messageKey), objArray(parameter)); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/AbstractStaticReplyCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/AbstractStaticReplyCommandHandlerTest.java new file mode 100644 index 0000000..65a37f7 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/AbstractStaticReplyCommandHandlerTest.java @@ -0,0 +1,121 @@ +/* + * 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.test.core.command; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.AbstractStaticReplyCommandHandler; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.InvocationRecord; +import org.xbib.files.ftp.mock.core.session.Session; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.test.AbstractTestCase; + +import java.util.ListResourceBundle; +import java.util.ResourceBundle; + +/** + * Tests for the AbstractStaticReplyCommandHandler class. + * + * @author Chris Mair + */ +class AbstractStaticReplyCommandHandlerTest extends AbstractTestCase { + + private static final int REPLY_CODE1 = 777; + private static final int REPLY_CODE2 = 888; + private static final String REPLY_TEXT1 = "reply1 ... abcdef"; + private static final String REPLY_TEXT2 = "abc {0} def"; + private static final String REPLY_TEXT2_FORMATTED = "abc 123 def"; + private static final String MESSAGE_KEY = "key.123"; + private static final String MESSAGE_TEXT = "message.123"; + private static final String OVERRIDE_REPLY_TEXT = "overridden reply ... abcdef"; + private static final Object ARG = "123"; + + private AbstractStaticReplyCommandHandler commandHandler; + private Session session; + + + @Test + void testSendReply() { + commandHandler.setReplyCode(REPLY_CODE1); + commandHandler.sendReply(session); + verify(session).sendReply(REPLY_CODE1, REPLY_TEXT1); + } + + @Test + void testSendReply_SetReplyText() { + commandHandler.setReplyCode(REPLY_CODE1); + commandHandler.setReplyText(OVERRIDE_REPLY_TEXT); + commandHandler.sendReply(session); + verify(session).sendReply(REPLY_CODE1, OVERRIDE_REPLY_TEXT); + } + + @Test + void testSendReply_SetReplyMessageKey() { + commandHandler.setReplyCode(REPLY_CODE1); + commandHandler.setReplyMessageKey(Integer.toString(REPLY_CODE2)); + commandHandler.sendReply(session); + + verify(session).sendReply(REPLY_CODE1, REPLY_TEXT2); + } + + @Test + void testSendReply_ReplyCodeNotSet() { + assertThrows(AssertFailedException.class, () -> commandHandler.sendReply(session)); + } + + @Test + void testSendReply_MessageParameter() { + commandHandler.setReplyCode(REPLY_CODE2); + commandHandler.sendReply(session); + commandHandler.sendReply(session, ARG); + + verify(session).sendReply(REPLY_CODE2, REPLY_TEXT2); + verify(session).sendReply(REPLY_CODE2, REPLY_TEXT2_FORMATTED); + } + + @Test + void testSetReplyCode_Invalid() { + assertThrows(AssertFailedException.class, () -> commandHandler.setReplyCode(0)); + } + + //------------------------------------------------------------------------- + // Test setup + //------------------------------------------------------------------------- + + @BeforeEach + void setUp() { + session = mock(Session.class); + commandHandler = new AbstractStaticReplyCommandHandler() { + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + } + }; + ResourceBundle replyTextBundle = new ListResourceBundle() { + protected Object[][] getContents() { + return new Object[][]{ + {Integer.toString(REPLY_CODE1), REPLY_TEXT1}, + {Integer.toString(REPLY_CODE2), REPLY_TEXT2}, + {MESSAGE_KEY, MESSAGE_TEXT} + }; + } + }; + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/AbstractTrackingCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/AbstractTrackingCommandHandlerTest.java new file mode 100644 index 0000000..9096c17 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/AbstractTrackingCommandHandlerTest.java @@ -0,0 +1,154 @@ +/* + * 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.test.core.command; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.AbstractTrackingCommandHandler; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.InvocationRecord; +import org.xbib.files.ftp.mock.core.session.Session; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.test.AbstractTestCase; + +import java.util.ListResourceBundle; +import java.util.ResourceBundle; + +/** + * Tests for the AbstractTrackingCommandHandler class. + * + * @author Chris Mair + */ +class AbstractTrackingCommandHandlerTest extends AbstractTestCase { + + private static final String COMMAND_NAME = "abc"; + private static final Object ARG = "123"; + private static final Object[] ARGS = {ARG}; + private static final Command COMMAND = new Command(COMMAND_NAME, EMPTY); + private static final Command COMMAND_WITH_ARGS = new Command(COMMAND_NAME, EMPTY); + private static final int REPLY_CODE1 = 777; + private static final int REPLY_CODE2 = 888; + private static final int REPLY_CODE3 = 999; + private static final String REPLY_TEXT1 = "reply1 ... abcdef"; + private static final String REPLY_TEXT2 = "abc {0} def"; + private static final String REPLY_TEXT2_FORMATTED = "abc 123 def"; + private static final String OVERRIDE_REPLY_TEXT = "overridden reply ... abcdef"; + private static final String MESSAGE_KEY = "key.123"; + private static final String MESSAGE_TEXT = "message.123"; + + private AbstractTrackingCommandHandler commandHandler; + private Session session; + + @Test + void testHandleCommand() throws Exception { + assertEquals(0, commandHandler.numberOfInvocations()); + commandHandler.handleCommand(COMMAND, session); + assertEquals(1, commandHandler.numberOfInvocations()); + assertTrue(commandHandler.getInvocation(0).isLocked()); + } + + @Test + void testHandleCommand_NullCommand() { + assertThrows(AssertFailedException.class, () -> commandHandler.handleCommand(null, session)); + } + + @Test + void testHandleCommand_NullSession() { + assertThrows(AssertFailedException.class, () -> commandHandler.handleCommand(COMMAND, null)); + } + + @Test + void testInvocationHistory() throws Exception { + assertEquals(0, commandHandler.numberOfInvocations()); + commandHandler.handleCommand(COMMAND, session); + assertEquals(1, commandHandler.numberOfInvocations()); + commandHandler.handleCommand(COMMAND, session); + assertEquals(2, commandHandler.numberOfInvocations()); + commandHandler.clearInvocations(); + assertEquals(0, commandHandler.numberOfInvocations()); + } + + @Test + void testGetInvocation() throws Exception { + commandHandler.handleCommand(COMMAND, session); + commandHandler.handleCommand(COMMAND_WITH_ARGS, session); + assertSame(COMMAND, commandHandler.getInvocation(0).getCommand()); + assertSame(COMMAND_WITH_ARGS, commandHandler.getInvocation(1).getCommand()); + } + + @Test + void testGetInvocation_IndexOutOfBounds() throws Exception { + commandHandler.handleCommand(COMMAND, session); + assertThrows(IndexOutOfBoundsException.class, () -> commandHandler.getInvocation(2)); + } + + @Test + void testSendReply() { + commandHandler.sendReply(session, REPLY_CODE1, null, null, null); + commandHandler.sendReply(session, REPLY_CODE1, MESSAGE_KEY, null, null); + commandHandler.sendReply(session, REPLY_CODE1, MESSAGE_KEY, OVERRIDE_REPLY_TEXT, null); + commandHandler.sendReply(session, REPLY_CODE3, null, null, null); + + verify(session).sendReply(REPLY_CODE1, REPLY_TEXT1); + verify(session).sendReply(REPLY_CODE1, MESSAGE_TEXT); + verify(session).sendReply(REPLY_CODE1, OVERRIDE_REPLY_TEXT); + verify(session).sendReply(REPLY_CODE3, null); + } + + @Test + void testSendReply_WithMessageArguments() { + commandHandler.sendReply(session, REPLY_CODE1, null, REPLY_TEXT2, ARGS); + + verify(session).sendReply(REPLY_CODE1, REPLY_TEXT2_FORMATTED); + } + + @Test + void testSendReply_NullSession() { + assertThrows(AssertFailedException.class, () -> commandHandler.sendReply(null, REPLY_CODE1, REPLY_TEXT1, null, null)); + } + + @Test + void testSendReply_InvalidReplyCode() { + assertThrows(AssertFailedException.class, () -> commandHandler.sendReply(session, 0, REPLY_TEXT1, null, null)); + } + + //------------------------------------------------------------------------- + // Test setup + //------------------------------------------------------------------------- + + @BeforeEach + void setUp() { + session = mock(Session.class); + commandHandler = new AbstractTrackingCommandHandler() { + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + } + }; + ResourceBundle replyTextBundle = new ListResourceBundle() { + protected Object[][] getContents() { + return new Object[][]{ + {Integer.toString(REPLY_CODE1), REPLY_TEXT1}, + {Integer.toString(REPLY_CODE2), REPLY_TEXT2}, + {MESSAGE_KEY, MESSAGE_TEXT} + }; + } + }; + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/CommandTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/CommandTest.java new file mode 100644 index 0000000..15d5a33 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/CommandTest.java @@ -0,0 +1,141 @@ +/* + * 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.test.core.command; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.CommandSyntaxException; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.test.AbstractTestCase; + +import java.util.List; + +/** + * Tests for the Command class + * + * @author Chris Mair + */ +class CommandTest extends AbstractTestCase { + + @Test + void testConstructor() { + final String[] PARAMETERS = array("123"); + Command command = new Command("abc", PARAMETERS); + assertEquals("abc", command.getName()); + assertArrayEquals(PARAMETERS, command.getParameters()); + } + + @Test + void testConstructor_List() { + final List PARAMETERS_LIST = list("123"); + final String[] PARAMETERS_ARRAY = array("123"); + Command command = new Command("abc", PARAMETERS_LIST); + assertEquals("abc", command.getName()); + assertArrayEquals(PARAMETERS_ARRAY, command.getParameters()); + } + + @Test + void testConstructor_NullName() { + assertThrows(AssertFailedException.class, () -> new Command(null, EMPTY)); + } + + @Test + void testConstructor_NullParameters() { + assertThrows(AssertFailedException.class, () -> new Command("OK", (String[]) null)); + } + + @Test + void testNormalizeName() { + assertEquals("XXX", Command.normalizeName("XXX")); + assertEquals("XXX", Command.normalizeName("xxx")); + assertEquals("XXX", Command.normalizeName("Xxx")); + } + + @Test + void testGetRequiredParameter() { + Command command = new Command("abc", array("123", "456")); + assertEquals("123", command.getRequiredParameter(0)); + assertEquals("456", command.getRequiredParameter(1)); + } + + @Test + void testGetRequiredParameter_IndexNotValid() { + Command command = new Command("abc", array("123", "456")); + assertThrows(CommandSyntaxException.class, () -> command.getRequiredParameter(2)); + } + + @Test + void testGetOptionalString() { + Command command = new Command("abc", array("123", "456")); + assertEquals("123", command.getOptionalString(0)); + assertEquals("456", command.getOptionalString(1)); + assertEquals(null, command.getOptionalString(2)); + } + + @Test + void testGetParameter() { + Command command = new Command("abc", array("123", "456")); + assertEquals("123", command.getParameter(0)); + assertEquals("456", command.getParameter(1)); + assertEquals(null, command.getParameter(2)); + } + + @Test + void testImmutable_ChangeOriginalParameters() { + final String[] PARAMETERS = {"a", "b", "c"}; + final Command COMMAND = new Command("command", PARAMETERS); + PARAMETERS[2] = "xxx"; + assertArrayEquals(COMMAND.getParameters(), new String[]{"a", "b", "c"}); + } + + @Test + void testImmutable_ChangeRetrievedParameters() { + final String[] PARAMETERS = {"a", "b", "c"}; + final Command COMMAND = new Command("command", PARAMETERS); + String[] parameters = COMMAND.getParameters(); + parameters[2] = "xxx"; + assertArrayEquals(PARAMETERS, COMMAND.getParameters()); + } + + @Test + void testEquals() { + final Command COMMAND1 = new Command("a", EMPTY); + final Command COMMAND2 = new Command("a", EMPTY); + final Command COMMAND3 = new Command("b", array("1")); + final Command COMMAND4 = new Command("b", array("2")); + final Command COMMAND5 = new Command("c", array("1")); + doTestEquals(COMMAND1, null, false); + doTestEquals(COMMAND1, COMMAND1, true); + doTestEquals(COMMAND1, COMMAND2, true); + doTestEquals(COMMAND1, COMMAND3, false); + doTestEquals(COMMAND3, COMMAND4, false); + doTestEquals(COMMAND3, COMMAND5, false); + } + + /** + * Test that command1 equals command2 if and only if expectedEqual is true + * + * @param command1 - the first command + * @param command2 - the second command + * @param expectedEqual - true if command1 is expected to equal command2 + */ + private void doTestEquals(Command command1, Command command2, boolean expectedEqual) { + assertEquals(expectedEqual, command1.equals(command2)); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/ConnectCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/ConnectCommandHandlerTest.java new file mode 100644 index 0000000..8672f9f --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/ConnectCommandHandlerTest.java @@ -0,0 +1,54 @@ +/* + * 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.test.core.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +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.ReplyCodes; + +/** + * Tests for the ConnectCommandHandler class + * + * @author Chris Mair + */ +class ConnectCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private ConnectCommandHandler commandHandler; + private Command command1; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + + verify(session).sendReply(ReplyCodes.CONNECT_OK, replyTextFor(ReplyCodes.CONNECT_OK)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyNoDataElements(commandHandler.getInvocation(0)); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new ConnectCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.CONNECT, EMPTY); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/InvocationRecordTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/InvocationRecordTest.java new file mode 100644 index 0000000..5c383dd --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/InvocationRecordTest.java @@ -0,0 +1,150 @@ +/* + * 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.test.core.command; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.InvocationRecord; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.test.AbstractTestCase; + +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +/** + * Tests for InvocationRecord + * + * @author Chris Mair + */ +class InvocationRecordTest extends AbstractTestCase { + + private static final Command COMMAND = new Command("command", EMPTY); + private static final String KEY1 = "key1"; + private static final String KEY2 = "key2"; + private static final String STRING = "abc123"; + private static final Integer INT = 77; + + private InvocationRecord invocationRecord; + + @Test + void testConstructor() { + final Command COMMAND = new Command("ABC", EMPTY); + long beforeTime = System.currentTimeMillis(); + InvocationRecord commandInvocation = new InvocationRecord(COMMAND, DEFAULT_HOST); + long afterTime = System.currentTimeMillis(); + log(commandInvocation.toString()); + assertEquals(COMMAND, commandInvocation.getCommand()); + assertTrue(commandInvocation.getTime().getTime() >= beforeTime + && commandInvocation.getTime().getTime() <= afterTime); + assertEquals(DEFAULT_HOST, commandInvocation.getClientHost()); + } + + @Test + void testSet_NullKey() { + assertThrows(AssertFailedException.class, () -> invocationRecord.set(null, STRING)); + } + + @Test + void testSet_NullValue() { + invocationRecord.set(KEY1, null); + assertNull(invocationRecord.getObject(KEY1)); + } + + @Test + void testContainsKey() { + invocationRecord.set(KEY1, STRING); + assertTrue(invocationRecord.containsKey(KEY1)); + assertFalse(invocationRecord.containsKey(KEY2)); + } + + @Test + void testContainsKey_Null() { + assertThrows(AssertFailedException.class, () -> invocationRecord.containsKey(null)); + } + + @Test + void testGetString() { + assertNull(invocationRecord.getString("UNDEFINED")); + invocationRecord.set(KEY1, STRING); + assertEquals(STRING, invocationRecord.getString(KEY1)); + } + + @Test + void testGetString_Null() { + assertThrows(AssertFailedException.class, () -> invocationRecord.getString(null)); + } + + @Test + void testGetString_NotAString() { + invocationRecord.set(KEY1, INT); + assertThrows(ClassCastException.class, () -> invocationRecord.getString(KEY1)); + } + + @Test + void testGetObject() { + assertNull(invocationRecord.getObject("UNDEFINED")); + invocationRecord.set(KEY1, STRING); + assertEquals(STRING, invocationRecord.getObject(KEY1)); + } + + @Test + void testKeySet() { + Set set = new HashSet(); + assertEquals(set, invocationRecord.keySet()); + invocationRecord.set(KEY1, STRING); + invocationRecord.set(KEY2, STRING); + set.add(KEY1); + set.add(KEY2); + assertEquals(set, invocationRecord.keySet()); + } + + @Test + void testKeySet_Immutability() { + Set keySet = invocationRecord.keySet(); + assertThrows(UnsupportedOperationException.class, () -> keySet.add("abc")); + } + + @Test + void testGetObject_Null() { + assertThrows(AssertFailedException.class, () -> invocationRecord.getObject(null)); + } + + @Test + void testLock() { + assertFalse(invocationRecord.isLocked()); + invocationRecord.set(KEY1, STRING); + invocationRecord.lock(); + assertTrue(invocationRecord.isLocked()); + assertThrows(AssertFailedException.class, () -> invocationRecord.set(KEY2, "abc")); + } + + @Test + void testGetTime_Immutability() { + Date timestamp = invocationRecord.getTime(); + long timeInMillis = timestamp.getTime(); + timestamp.setTime(12345L); + assertEquals(timeInMillis, invocationRecord.getTime().getTime()); + } + + @BeforeEach + void setUp() { + invocationRecord = new InvocationRecord(COMMAND, DEFAULT_HOST); + } +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/ReplyTextBundleUtilTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/ReplyTextBundleUtilTest.java new file mode 100644 index 0000000..721d980 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/ReplyTextBundleUtilTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2021 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.test.core.command; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.AbstractTrackingCommandHandler; +import org.xbib.files.ftp.mock.core.command.CommandHandler; +import org.xbib.files.ftp.mock.core.command.ReplyTextBundleUtil; +import org.xbib.files.ftp.mock.core.command.StaticReplyCommandHandler; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.test.AbstractTestCase; + +import java.util.ListResourceBundle; +import java.util.ResourceBundle; + +/** + * Tests for the ReplyTextBundleUtil class. + * + * @author Chris Mair + */ +class ReplyTextBundleUtilTest extends AbstractTestCase { + + private ResourceBundle resourceBundle1; + private ResourceBundle resourceBundle2; + + @Test + void testSetReplyTextBundleIfAppropriate_ReplyTextBundleAware_NotSetYet() { + AbstractTrackingCommandHandler commandHandler = new StaticReplyCommandHandler(); + ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, resourceBundle1); + assertSame(resourceBundle1, commandHandler.getReplyTextBundle()); + } + + @Test + void testSetReplyTextBundleIfAppropriate_ReplyTextBundleAware_AlreadySet() { + AbstractTrackingCommandHandler commandHandler = new StaticReplyCommandHandler(); + commandHandler.setReplyTextBundle(resourceBundle2); + ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, resourceBundle1); + assertSame(resourceBundle2, commandHandler.getReplyTextBundle()); + } + + @Test + void testSetReplyTextBundleIfAppropriate_NotReplyTextBundleAware() { + CommandHandler commandHandler = mock(CommandHandler.class); + ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, resourceBundle1); + verifyNoInteractions(commandHandler); // expect no method calls + } + + @Test + void testSetReplyTextBundleIfAppropriate_NullCommandHandler() { + assertThrows(AssertFailedException.class, () -> ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(null, resourceBundle1)); + } + + @BeforeEach + void setUp() { + resourceBundle1 = new ListResourceBundle() { + protected Object[][] getContents() { + return null; + } + }; + + resourceBundle2 = new ListResourceBundle() { + protected Object[][] getContents() { + return new Object[][] { { "a", "b" } }; + } + }; + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/SimpleCompositeCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/SimpleCompositeCommandHandlerTest.java new file mode 100644 index 0000000..86df995 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/SimpleCompositeCommandHandlerTest.java @@ -0,0 +1,184 @@ +/* + * Copyright 2021 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.test.core.command; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.AbstractTrackingCommandHandler; +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.SimpleCompositeCommandHandler; +import org.xbib.files.ftp.mock.core.command.StaticReplyCommandHandler; +import org.xbib.files.ftp.mock.core.session.Session; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.test.AbstractTestCase; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListResourceBundle; +import java.util.ResourceBundle; + +/** + * Tests for SimpleCompositeCommandHandler + * + * @author Chris Mair + */ +class SimpleCompositeCommandHandlerTest extends AbstractTestCase { + + private SimpleCompositeCommandHandler simpleCompositeCommandHandler; + private Session session; + private Command command; + private CommandHandler commandHandler1; + private CommandHandler commandHandler2; + private CommandHandler commandHandler3; + + @Test + void testHandleCommand_OneHandler_OneInvocation() throws Exception { + simpleCompositeCommandHandler.addCommandHandler(commandHandler1); + + simpleCompositeCommandHandler.handleCommand(command, session); + + verify(commandHandler1).handleCommand(command, session); + } + + @Test + void testHandleCommand_TwoHandlers() throws Exception { + simpleCompositeCommandHandler.addCommandHandler(commandHandler1); + simpleCompositeCommandHandler.addCommandHandler(commandHandler2); + + simpleCompositeCommandHandler.handleCommand(command, session); + simpleCompositeCommandHandler.handleCommand(command, session); + + verify(commandHandler1).handleCommand(command, session); + verify(commandHandler2).handleCommand(command, session); + } + + @Test + void testHandleCommand_ThreeHandlers() throws Exception { + List list = new ArrayList(); + list.add(commandHandler1); + list.add(commandHandler2); + list.add(commandHandler3); + simpleCompositeCommandHandler.setCommandHandlers(list); + + simpleCompositeCommandHandler.handleCommand(command, session); + simpleCompositeCommandHandler.handleCommand(command, session); + simpleCompositeCommandHandler.handleCommand(command, session); + + verify(commandHandler1).handleCommand(command, session); + verify(commandHandler2).handleCommand(command, session); + verify(commandHandler3).handleCommand(command, session); + } + + @Test + void testHandleCommand_OneHandler_TooManyInvocations() throws Exception { + simpleCompositeCommandHandler.addCommandHandler(commandHandler1); + + simpleCompositeCommandHandler.handleCommand(command, session); + + verify(commandHandler1).handleCommand(command, session); + + // Second invocation throws an exception + assertThrows(AssertFailedException.class, () -> simpleCompositeCommandHandler.handleCommand(command, session)); + } + + @Test + void testHandleCommand_NoHandlersDefined() { + assertThrows(AssertFailedException.class, () -> simpleCompositeCommandHandler.handleCommand(command, session)); + } + + @Test + void testHandleCommand_NullCommand() { + assertThrows(AssertFailedException.class, () -> simpleCompositeCommandHandler.handleCommand(null, session)); + } + + @Test + void testHandleCommand_NullSession() { + assertThrows(AssertFailedException.class, () -> simpleCompositeCommandHandler.handleCommand(command, null)); + } + + @Test + void testAddCommandHandler_NullCommandHandler() { + assertThrows(AssertFailedException.class, () -> simpleCompositeCommandHandler.addCommandHandler(null)); + } + + @Test + void testSetCommandHandlers_Null() { + assertThrows(AssertFailedException.class, () -> simpleCompositeCommandHandler.setCommandHandlers(null)); + } + + @Test + void testGetCommandHandler_UndefinedIndex() { + simpleCompositeCommandHandler.addCommandHandler(commandHandler1); + assertThrows(AssertFailedException.class, () -> simpleCompositeCommandHandler.getCommandHandler(1)); + } + + @Test + void testGetCommandHandler() { + simpleCompositeCommandHandler.addCommandHandler(commandHandler1); + simpleCompositeCommandHandler.addCommandHandler(commandHandler2); + assertSame(commandHandler1, simpleCompositeCommandHandler.getCommandHandler(0)); + assertSame(commandHandler2, simpleCompositeCommandHandler.getCommandHandler(1)); + } + + @Test + void testGetCommandHandler_NegativeIndex() { + simpleCompositeCommandHandler.addCommandHandler(commandHandler1); + assertThrows(AssertFailedException.class, () -> simpleCompositeCommandHandler.getCommandHandler(-1)); + } + + @Test + void testGetReplyTextBundle() { + assertNull(simpleCompositeCommandHandler.getReplyTextBundle()); + } + + @Test + void testSetReplyTextBundle() { + AbstractTrackingCommandHandler replyTextBundleAwareCommandHandler1 = new StaticReplyCommandHandler(); + AbstractTrackingCommandHandler replyTextBundleAwareCommandHandler2 = new StaticReplyCommandHandler(); + simpleCompositeCommandHandler.addCommandHandler(replyTextBundleAwareCommandHandler1); + simpleCompositeCommandHandler.addCommandHandler(commandHandler1); + simpleCompositeCommandHandler.addCommandHandler(replyTextBundleAwareCommandHandler2); + + ResourceBundle resourceBundle = new ListResourceBundle() { + protected Object[][] getContents() { + return null; + } + }; + + simpleCompositeCommandHandler.setReplyTextBundle(resourceBundle); + assertSame(resourceBundle, replyTextBundleAwareCommandHandler1.getReplyTextBundle()); + assertSame(resourceBundle, replyTextBundleAwareCommandHandler1.getReplyTextBundle()); + } + + //------------------------------------------------------------------------- + // Test setup + //------------------------------------------------------------------------- + + @BeforeEach + void setUp() { + simpleCompositeCommandHandler = new SimpleCompositeCommandHandler(); + session = mock(Session.class); + command = new Command("cmd", EMPTY); + commandHandler1 = mock(CommandHandler.class); + commandHandler2 = mock(CommandHandler.class); + commandHandler3 = mock(CommandHandler.class); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/StaticReplyCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/StaticReplyCommandHandlerTest.java new file mode 100644 index 0000000..9e07d02 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/StaticReplyCommandHandlerTest.java @@ -0,0 +1,92 @@ +/* + * 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.test.core.command; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.StaticReplyCommandHandler; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; + +/** + * Tests for the StaticReplyCommandHandler class + * + * @author Chris Mair + */ +class StaticReplyCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final int REPLY_CODE = 999; + private static final String REPLY_TEXT = "some text 123"; + private static final Command COMMAND = new Command("ANY", EMPTY); + + private StaticReplyCommandHandler commandHandler; + + @Test + void testConstructor_String_InvalidReplyCode() { + assertThrows(AssertFailedException.class, () -> new StaticReplyCommandHandler(-1)); + } + + @Test + void testConstructor_StringString_InvalidReplyCode() { + assertThrows(AssertFailedException.class, () -> new StaticReplyCommandHandler(-99, "text")); + } + + @Test + void testSetReplyCode_Invalid() { + assertThrows(AssertFailedException.class, () -> commandHandler.setReplyCode(-1)); + } + + @Test + void testHandleCommand_ReplyTextNotSet() throws Exception { + commandHandler.setReplyCode(250); + + commandHandler.handleCommand(COMMAND, session); + + verify(session).sendReply(250, replyTextFor(250)); + verifyNumberOfInvocations(commandHandler, 1); + verifyNoDataElements(commandHandler.getInvocation(0)); + } + + @Test + void testHandleCommand_SetReplyText() throws Exception { + commandHandler.setReplyCode(REPLY_CODE); + commandHandler.setReplyText(REPLY_TEXT); + + commandHandler.handleCommand(COMMAND, session); + + verify(session).sendReply(REPLY_CODE, REPLY_TEXT); + verifyNumberOfInvocations(commandHandler, 1); + verifyNoDataElements(commandHandler.getInvocation(0)); + } + + @Test + void testHandleCommand_ReplyCodeNotSet() throws Exception { + assertThrows(AssertFailedException.class, () -> commandHandler.handleCommand(COMMAND, session)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyNoDataElements(commandHandler.getInvocation(0)); + } + + @BeforeEach + void setUp() { + commandHandler = new StaticReplyCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/UnsupportedCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/UnsupportedCommandHandlerTest.java new file mode 100644 index 0000000..d200d86 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/command/UnsupportedCommandHandlerTest.java @@ -0,0 +1,53 @@ +/* + * 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.test.core.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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.command.UnsupportedCommandHandler; + +/** + * Tests for the UnsupportedCommandHandler class + * + * @author Chris Mair + */ +class UnsupportedCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private UnsupportedCommandHandler commandHandler; + private Command command1; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + + verify(session).sendReply(ReplyCodes.COMMAND_NOT_SUPPORTED, replyTextFor(ReplyCodes.COMMAND_NOT_SUPPORTED)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyNoDataElements(commandHandler.getInvocation(0)); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new UnsupportedCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command("XXXX", EMPTY); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/server/AbstractFtpServerTestCase.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/server/AbstractFtpServerTestCase.java new file mode 100644 index 0000000..39eda77 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/server/AbstractFtpServerTestCase.java @@ -0,0 +1,141 @@ +/* + * 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.test.core.server; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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.server.AbstractFtpServer; +import org.xbib.files.ftp.mock.core.session.DefaultSession; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.test.AbstractTestCase; + +import java.net.Socket; +import java.util.HashMap; +import java.util.Map; + +/** + * Abstract superclass for tests of AbstractFtpServer subclasses. + * + * @author Chris Mair + */ +public abstract class AbstractFtpServerTestCase extends AbstractTestCase { + + protected AbstractFtpServer ftpServer; + private CommandHandler commandHandler; + private CommandHandler commandHandler2; + + @Test + void testSetCommandHandlers() { + Map mapping = new HashMap(); + mapping.put("AAA", commandHandler); + mapping.put("BBB", commandHandler2); + + ftpServer.setCommandHandlers(mapping); + assertSame(commandHandler, ftpServer.getCommandHandler("AAA")); + assertSame(commandHandler2, ftpServer.getCommandHandler("BBB")); + + verifyCommandHandlerInitialized(commandHandler); + verifyCommandHandlerInitialized(commandHandler2); + + // Make sure default CommandHandlers are still set + assertTrue(ftpServer.getCommandHandler(CommandNames.CONNECT) != null); + } + + @Test + void testSetCommandHandlers_Null() { + assertThrows(AssertFailedException.class, () -> ftpServer.setCommandHandlers(null)); + } + + @Test + void testSetCommandHandler() { + ftpServer.setCommandHandler("ZZZ", commandHandler2); + assertSame(commandHandler2, ftpServer.getCommandHandler("ZZZ")); + verifyCommandHandlerInitialized(commandHandler2); + } + + @Test + void testSetCommandHandler_NullCommandName() { + CommandHandler commandHandler = mock(CommandHandler.class); + assertThrows(AssertFailedException.class, () -> ftpServer.setCommandHandler(null, commandHandler)); + } + + @Test + void testSetCommandHandler_NullCommandHandler() { + assertThrows(AssertFailedException.class, () -> ftpServer.setCommandHandler("ZZZ", null)); + } + + @Test + void testSetServerControlPort() { + assertEquals(21, ftpServer.getServerControlPort()); + ftpServer.setServerControlPort(99); + assertEquals(99, ftpServer.getServerControlPort()); + } + + @Test + void testLowerCaseOrMixedCaseCommandNames() { + ftpServer.setCommandHandler("XXX", commandHandler); + assertSame(commandHandler, ftpServer.getCommandHandler("XXX")); + assertSame(commandHandler, ftpServer.getCommandHandler("Xxx")); + assertSame(commandHandler, ftpServer.getCommandHandler("xxx")); + + ftpServer.setCommandHandler("YyY", commandHandler); + assertSame(commandHandler, ftpServer.getCommandHandler("YYY")); + assertSame(commandHandler, ftpServer.getCommandHandler("Yyy")); + assertSame(commandHandler, ftpServer.getCommandHandler("yyy")); + + ftpServer.setCommandHandler("zzz", commandHandler); + assertSame(commandHandler, ftpServer.getCommandHandler("ZZZ")); + assertSame(commandHandler, ftpServer.getCommandHandler("zzZ")); + assertSame(commandHandler, ftpServer.getCommandHandler("zzz")); + } + + @Test + void testStopWithoutStart() { + ftpServer.stop(); + } + + @Test + void testCreateSession() { + assertEquals(ftpServer.createSession(new Socket()).getClass(), DefaultSession.class); + } + + //------------------------------------------------------------------------- + // Test setup + //------------------------------------------------------------------------- + + @BeforeEach + void setUp_AbstractFtpServerTestCase() { + ftpServer = createFtpServer(); + commandHandler = createCommandHandler(); + commandHandler2 = createCommandHandler(); + } + + //------------------------------------------------------------------------- + // Abstract method declarations + //------------------------------------------------------------------------- + + protected abstract AbstractFtpServer createFtpServer(); + + protected abstract CommandHandler createCommandHandler(); + + protected abstract void verifyCommandHandlerInitialized(CommandHandler commandHandler); + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/server/AbstractFtpServer_StartTestCase.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/server/AbstractFtpServer_StartTestCase.java new file mode 100644 index 0000000..fe336ed --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/server/AbstractFtpServer_StartTestCase.java @@ -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.test.core.server; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.server.AbstractFtpServer; +import org.xbib.files.ftp.mock.test.AbstractTestCase; +import org.xbib.files.ftp.mock.test.PortTestUtil; +import org.xbib.io.ftp.client.FTPClient; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Abstract superclass for tests of AbstractFtpServer subclasses that require the server thread to be started. + * + * @author Chris Mair + */ +public abstract class AbstractFtpServer_StartTestCase extends AbstractTestCase { + + private static final String SERVER = "localhost"; + + private AbstractFtpServer ftpServer; + + @Test + void testStartAndStop() throws Exception { + ftpServer.setServerControlPort(PortTestUtil.getFtpServerControlPort()); + assertEquals(false, ftpServer.isStarted()); + + ftpServer.start(); + Thread.sleep(200L); // give it some time to get started + assertEquals(true, ftpServer.isStarted()); + assertEquals(false, ftpServer.isShutdown()); + + ftpServer.stop(); + + assertEquals(true, ftpServer.isShutdown()); + } + + @Test + void testCustomServerControlPort() throws Exception { + final int SERVER_CONTROL_PORT = 9187; + + ftpServer.setServerControlPort(SERVER_CONTROL_PORT); + ftpServer.start(); + + try { + FTPClient ftpClient = new FTPClient(); + ftpClient.connect(SERVER, SERVER_CONTROL_PORT); + } + finally { + ftpServer.stop(); + } + } + + //------------------------------------------------------------------------- + // Test setup + //------------------------------------------------------------------------- + + @BeforeEach + void setUp() throws Exception { + ftpServer = createFtpServer(); + } + + //------------------------------------------------------------------------- + // Abstract method declarations + //------------------------------------------------------------------------- + + protected abstract AbstractFtpServer createFtpServer(); + +} \ No newline at end of file diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/session/DefaultSessionTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/session/DefaultSessionTest.java new file mode 100644 index 0000000..e7ad09e --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/session/DefaultSessionTest.java @@ -0,0 +1,412 @@ +/* + * 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.test.core.session; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.xbib.files.ftp.mock.core.MockFtpServerException; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.session.DefaultSession; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.test.core.socket.StubServerSocket; +import org.xbib.files.ftp.mock.test.core.socket.StubServerSocketFactory; +import org.xbib.files.ftp.mock.test.core.socket.StubSocket; +import org.xbib.files.ftp.mock.test.core.socket.StubSocketFactory; +import org.xbib.files.ftp.mock.test.AbstractTestCase; + +import java.io.*; +import java.net.InetAddress; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Tests for the DefaultSession class + * + * @author Chris Mair + */ +class DefaultSessionTest extends AbstractTestCase { + + private static final String DATA = "sample data 123"; + private static final int PORT = 197; + private static final String NAME1 = "name1"; + private static final String NAME2 = "name2"; + private static final Object VALUE = "value"; + private static final String COMMAND = "LIST"; + + private DefaultSession session; + private ByteArrayOutputStream outputStream; + private Map commandHandlerMap; + private StubSocket stubSocket; + private InetAddress clientHost; + + @Test + void testConstructor_NullControlSocket() { + assertThrows(AssertFailedException.class, () -> new DefaultSession(null, commandHandlerMap)); + } + + @Test + void testConstructor_NullCommandHandlerMap() { + assertThrows(AssertFailedException.class, () -> new DefaultSession(stubSocket, null)); + } + + @Test + void testSetClientDataPort() { + StubSocket stubSocket = createTestSocket(""); + StubSocketFactory stubSocketFactory = new StubSocketFactory(stubSocket); + session.socketFactory = stubSocketFactory; + session.setClientDataPort(PORT); + session.setClientDataHost(clientHost); + session.openDataConnection(); + assertEquals(PORT, stubSocketFactory.requestedDataPort); + } + + @Test + void testSetClientDataPort_AfterPassiveConnectionMode() throws IOException { + StubServerSocket stubServerSocket = new StubServerSocket(PORT); + StubServerSocketFactory stubServerSocketFactory = new StubServerSocketFactory(stubServerSocket); + session.serverSocketFactory = stubServerSocketFactory; + + session.switchToPassiveMode(); + assertFalse(stubServerSocket.isClosed()); + assertNotNull(session.passiveModeDataSocket); + session.setClientDataPort(PORT); + + // Make sure that any passive mode connection info is cleared out + assertTrue(stubServerSocket.isClosed()); + assertNull(session.passiveModeDataSocket); + } + + @Test + void testSetClientHost() throws Exception { + StubSocket stubSocket = createTestSocket(""); + StubSocketFactory stubSocketFactory = new StubSocketFactory(stubSocket); + session.socketFactory = stubSocketFactory; + session.setClientDataHost(clientHost); + session.openDataConnection(); + assertEquals(clientHost, stubSocketFactory.requestedHost); + } + + @Test + void testOpenDataConnection() { + StubSocket stubSocket = createTestSocket(""); + StubSocketFactory stubSocketFactory = new StubSocketFactory(stubSocket); + session.socketFactory = stubSocketFactory; + + // Use default client data port + session.setClientDataHost(clientHost); + session.openDataConnection(); + assertEquals(DefaultSession.DEFAULT_CLIENT_DATA_PORT, stubSocketFactory.requestedDataPort); + assertEquals(clientHost, stubSocketFactory.requestedHost); + + // Set client data port explicitly + session.setClientDataPort(PORT); + session.setClientDataHost(clientHost); + session.openDataConnection(); + assertEquals(PORT, stubSocketFactory.requestedDataPort); + assertEquals(clientHost, stubSocketFactory.requestedHost); + } + + @Test + void testOpenDataConnection_PassiveMode_NoConnection() throws IOException { + StubServerSocket stubServerSocket = new StubServerSocket(PORT); + StubServerSocketFactory stubServerSocketFactory = new StubServerSocketFactory(stubServerSocket); + session.serverSocketFactory = stubServerSocketFactory; + + session.switchToPassiveMode(); + + assertThrows(MockFtpServerException.class, () -> session.openDataConnection()); + } + + @Test + void testOpenDataConnection_NullClientHost() { + assertThrows(AssertFailedException.class, () -> session.openDataConnection()); + } + + @Test + void testReadData() { + StubSocket stubSocket = createTestSocket(DATA); + session.socketFactory = new StubSocketFactory(stubSocket); + session.setClientDataHost(clientHost); + + session.openDataConnection(); + byte[] data = session.readData(); + log("data=[" + new String(data) + "]"); + assertArrayEquals(DATA.getBytes(), data); + } + + @Test + void testReadData_PassiveMode() throws IOException { + StubSocket stubSocket = createTestSocket(DATA); + StubServerSocket stubServerSocket = new StubServerSocket(PORT, stubSocket); + StubServerSocketFactory stubServerSocketFactory = new StubServerSocketFactory(stubServerSocket); + session.serverSocketFactory = stubServerSocketFactory; + + session.switchToPassiveMode(); + session.openDataConnection(); + byte[] data = session.readData(); + log("data=[" + new String(data) + "]"); + assertArrayEquals(DATA.getBytes(), data); + } + + @ParameterizedTest + @ValueSource(ints = {1, 5, 15}) + void testReadData_NumBytes(int numBytes) { + final String EXPECTED_DATA = DATA.substring(0, numBytes); + StubSocket stubSocket = createTestSocket(DATA); + session.socketFactory = new StubSocketFactory(stubSocket); + session.setClientDataHost(clientHost); + + session.openDataConnection(); + byte[] data = session.readData(numBytes); + log("data=[" + new String(data) + "]"); + assertArrayEquals(EXPECTED_DATA.getBytes(), data); + } + + @Test + void testReadData_LargeData() { + final int NUM_BYTES = 50_000_000; + byte[] bytes = new byte[NUM_BYTES + 3]; + + StubSocket stubSocket = createTestSocket(bytes); + session.socketFactory = new StubSocketFactory(stubSocket); + session.setClientDataHost(clientHost); + session.openDataConnection(); + + Instant startTime = Instant.now(); + byte[] data = session.readData(NUM_BYTES); + Instant endTime = Instant.now(); + + Duration duration = Duration.between(startTime, endTime); + log("Read " + data.length + " bytes in " + duration.toMillis() + " milliseconds"); + assertEquals(NUM_BYTES, data.length); + } + + @Test + void testReadData_NumBytes_AskForMoreBytesThanThereAre() { + StubSocket stubSocket = createTestSocket(DATA); + session.socketFactory = new StubSocketFactory(stubSocket); + session.setClientDataHost(clientHost); + + session.openDataConnection(); + byte[] data = session.readData(10000); + log("data=[" + new String(data) + "]"); + assertArrayEquals(DATA.getBytes(), data); + } + + @Test + void testCloseDataConnection() { + StubSocket stubSocket = createTestSocket(DATA); + session.socketFactory = new StubSocketFactory(stubSocket); + + session.setClientDataHost(clientHost); + session.openDataConnection(); + session.closeDataConnection(); + assertTrue(stubSocket.isClosed()); + } + + @Test + void testCloseDataConnection_PassiveMode() throws IOException { + StubSocket stubSocket = createTestSocket(DATA); + StubServerSocket stubServerSocket = new StubServerSocket(1, stubSocket); + session.serverSocketFactory = new StubServerSocketFactory(stubServerSocket); + + session.switchToPassiveMode(); + session.setClientDataHost(clientHost); + session.openDataConnection(); + session.closeDataConnection(); + assertTrue(stubSocket.isClosed()); + assertTrue(stubServerSocket.isClosed()); + } + + @Test + void testSwitchToPassiveMode() throws IOException { + StubServerSocket stubServerSocket = new StubServerSocket(PORT); + StubServerSocketFactory stubServerSocketFactory = new StubServerSocketFactory(stubServerSocket); + session.serverSocketFactory = stubServerSocketFactory; + + assertNull(session.passiveModeDataSocket); + int port = session.switchToPassiveMode(); + assertSame(stubServerSocket, session.passiveModeDataSocket); + assertEquals(PORT, port); + } + + @Test + void testGetServerHost() { + assertEquals(DEFAULT_HOST, session.getServerHost()); + } + + @Test + void testGetClientHost_NotRunning() { + assertNull(session.getClientHost()); + } + + @Test + void testReadCommand() { + StringReader stringReader = new StringReader(COMMAND); + session.controlConnectionReader = new BufferedReader(stringReader); + assertEquals(new Command(COMMAND, EMPTY), session.readCommand()); + } + + @Test + void testReadCommand_ReadLineReturnsNull_ReturnsNull() { + session.controlConnectionReader = new BufferedReader(new StringReader(COMMAND)) { + public boolean ready() { return true; } + public String readLine() { return null; } + }; + assertNull(session.readCommand()); + } + + @Test + void testReadCommand_Closed_ReturnsNull() { + session.close(); + assertNull(session.readCommand()); + } + + @Test + void testParseCommand() { + Command command = session.parseCommand("LIST"); + assertEquals("LIST", command.getName()); + assertArrayEquals(EMPTY, command.getParameters()); + + command = session.parseCommand("USER user123"); + assertEquals("USER", command.getName()); + assertArrayEquals(array("user123"), command.getParameters()); + + command = session.parseCommand("PORT 127,0,0,1,17,37"); + assertEquals("PORT", command.getName()); + assertArrayEquals(new String[] { "127", "0", "0", "1", "17", "37" }, command.getParameters()); + } + + @Test + void testParseCommand_EmptyCommandString() { + assertThrows(AssertFailedException.class, () -> session.parseCommand("")); + } + + @Test + void testSendData() { + StubSocket stubSocket = createTestSocket("1234567890 abcdef"); + session.socketFactory = new StubSocketFactory(stubSocket); + + session.setClientDataHost(clientHost); + session.openDataConnection(); + session.sendData(DATA.getBytes(), DATA.length()); + log("output=[" + outputStream.toString() + "]"); + assertEquals(DATA, outputStream.toString()); + } + + @Test + void testSendData_Null() { + assertThrows(AssertFailedException.class, () -> session.sendData(null, 1)); + } + + @Test + void testSendReply_InvalidReplyCode() { + assertThrows(AssertFailedException.class, () -> session.sendReply(-66, "text")); + } + + @Test + void testGetAndSetAttribute() { + assertNull(session.getAttribute(NAME1)); + session.setAttribute(NAME1, VALUE); + session.setAttribute(NAME2, null); + assertEquals(VALUE, session.getAttribute(NAME1)); + assertNull(session.getAttribute(NAME2)); + assertNull(session.getAttribute("noSuchName")); + } + + @Test + void testGetAttribute_Null() { + assertThrows(AssertFailedException.class, () -> session.getAttribute(null)); + } + + @Test + void testSetAttribute_NullName() { + assertThrows(AssertFailedException.class, () -> session.setAttribute(null, VALUE)); + } + + @Test + void testRemoveAttribute() { + session.removeAttribute("noSuchName"); // do nothing + session.setAttribute(NAME1, VALUE); + session.removeAttribute(NAME1); + assertNull(session.getAttribute(NAME1)); + } + + @Test + void testRemoveAttribute_Null() { + assertThrows(AssertFailedException.class, () -> session.removeAttribute(null)); + } + + @Test + void testGetAttributeNames() { + assertEquals(Collections.EMPTY_SET, session.getAttributeNames()); + session.setAttribute(NAME1, VALUE); + assertEquals(Collections.singleton(NAME1), session.getAttributeNames()); + session.setAttribute(NAME2, VALUE); + assertEquals(set(NAME1, NAME2), session.getAttributeNames()); + } + + // ------------------------------------------------------------------------- + // Setup and Internal Helper Methods + // ------------------------------------------------------------------------- + + @BeforeEach + void setUp() throws Exception { + commandHandlerMap = new HashMap(); + outputStream = new ByteArrayOutputStream(); + session = createDefaultSession(""); + clientHost = InetAddress.getLocalHost(); + } + + /** + * Create and return a DefaultSession object that reads from an InputStream with the specified + * contents and writes to the predefined outputStrean ByteArrayOutputStream. Also, save the + * StubSocket being used in the stubSocket attribute. + * + * @param inputStreamContents - the contents of the input stream + * @return the DefaultSession + */ + private DefaultSession createDefaultSession(String inputStreamContents) { + stubSocket = createTestSocket(inputStreamContents); + return new DefaultSession(stubSocket, commandHandlerMap); + } + + /** + * Create and return a StubSocket that reads from an InputStream with the specified contents and + * writes to the predefined outputStrean ByteArrayOutputStream. + * + * @param inputStreamContents - the contents of the input stream + * @return the StubSocket + */ + private StubSocket createTestSocket(String inputStreamContents) { + return createTestSocket(inputStreamContents.getBytes()); + } + + private StubSocket createTestSocket(byte[] inputStreamContents) { + InputStream inputStream = new ByteArrayInputStream(inputStreamContents); + StubSocket stubSocket = new StubSocket(inputStream, outputStream); + stubSocket._setLocalAddress(DEFAULT_HOST); + return stubSocket; + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/session/DefaultSession_RunTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/session/DefaultSession_RunTest.java new file mode 100644 index 0000000..b62506e --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/session/DefaultSession_RunTest.java @@ -0,0 +1,256 @@ +/* + * 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.test.core.session; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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.command.ConnectCommandHandler; +import org.xbib.files.ftp.mock.core.command.InvocationRecord; +import org.xbib.files.ftp.mock.core.session.DefaultSession; +import org.xbib.files.ftp.mock.core.session.Session; +import org.xbib.files.ftp.mock.stub.command.AbstractStubCommandHandler; +import org.xbib.files.ftp.mock.test.core.socket.StubSocket; +import org.xbib.files.ftp.mock.test.AbstractTestCase; + +import java.io.*; +import java.util.HashMap; +import java.util.ListResourceBundle; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * Tests for the DefaultSession class that require the session (thread) to be running/active. + * + * @author Chris Mair + */ +class DefaultSession_RunTest extends AbstractTestCase { + + private static final Command COMMAND = new Command("USER", EMPTY); + private static final int REPLY_CODE = 100; + private static final String REPLY_TEXT = "sample text description"; + + private DefaultSession session; + private ByteArrayOutputStream outputStream; + private Map commandHandlerMap; + private StubSocket stubSocket; + private boolean commandHandled = false; + private String commandToRegister = COMMAND.getName(); + + @BeforeEach + void setUp() throws Exception { + commandHandlerMap = new HashMap(); + outputStream = new ByteArrayOutputStream(); + } + + @Test + void testInvocationOfCommandHandler() throws Exception { + AbstractStubCommandHandler commandHandler = new AbstractStubCommandHandler() { + public void handleCommand(Command command, Session cmdSession, InvocationRecord invocationRecord) { + assertEquals(COMMAND, command); + assertSame(session, cmdSession); + assertEquals(COMMAND, invocationRecord.getCommand()); + assertEquals(DEFAULT_HOST, invocationRecord.getClientHost()); + commandHandled = true; + } + }; + runCommandAndVerifyOutput(commandHandler, ""); + } + + @Test + void testClose() throws Exception { + CommandHandler commandHandler = new AbstractStubCommandHandler() { + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + session.close(); + commandHandled = true; + } + }; + runCommandAndVerifyOutput(commandHandler, ""); + assertFalse(stubSocket.isClosed()); + assertTrue(session.isClosed()); + } + + @Test + void testClose_WithoutCommand() throws Exception { + PipedOutputStream pipedOutputStream = new PipedOutputStream(); + PipedInputStream inputStream = new PipedInputStream(pipedOutputStream); + stubSocket = new StubSocket(DEFAULT_HOST, inputStream, outputStream); + session = new DefaultSession(stubSocket, commandHandlerMap); + + initializeConnectCommandHandler(); + + Thread thread = new Thread(session); + thread.start(); + Thread.sleep(1000L); + + session.close(); + thread.join(); + assertTrue(session.isClosed()); + } + + @Test + void testGetClientHost() throws Exception { + CommandHandler commandHandler = new AbstractStubCommandHandler() { + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + commandHandled = true; + } + }; + runCommandAndVerifyOutput(commandHandler, ""); + log("clientHost=" + session.getClientHost()); + assertEquals(DEFAULT_HOST, session.getClientHost()); + } + + @Test + void testSendReply_NullReplyText() throws Exception { + CommandHandler commandHandler = new AbstractStubCommandHandler() { + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + session.sendReply(REPLY_CODE, null); + commandHandled = true; + } + }; + runCommandAndVerifyOutput(commandHandler, Integer.toString(REPLY_CODE)); + } + + @Test + void testSendReply_TrimReplyText() throws Exception { + CommandHandler commandHandler = new AbstractStubCommandHandler() { + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + session.sendReply(REPLY_CODE, " " + REPLY_TEXT + " "); + commandHandled = true; + } + }; + runCommandAndVerifyOutput(commandHandler, REPLY_CODE + " " + REPLY_TEXT); + } + + @Test + void testSendReply_MultiLineText() throws Exception { + final String MULTILINE_REPLY_TEXT = "abc\ndef\nghi\njkl"; + final String FORMATTED_MULTILINE_REPLY_TEXT = "123-abc\ndef\nghi\n123 jkl"; + + CommandHandler commandHandler = new AbstractStubCommandHandler() { + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + session.sendReply(123, MULTILINE_REPLY_TEXT); + commandHandled = true; + } + }; + runCommandAndVerifyOutput(commandHandler, FORMATTED_MULTILINE_REPLY_TEXT); + } + + @Test + void testSendReply_ReplyText() throws Exception { + CommandHandler commandHandler = new AbstractStubCommandHandler() { + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + session.sendReply(REPLY_CODE, REPLY_TEXT); + commandHandled = true; + } + }; + runCommandAndVerifyOutput(commandHandler, REPLY_CODE + " " + REPLY_TEXT); + } + + @Test + void testUnrecognizedCommand() throws Exception { + // Register a handler for unsupported commands + CommandHandler commandHandler = new AbstractStubCommandHandler() { + public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + session.sendReply(502, "Unsupported"); + commandHandled = true; + } + }; + // Register the UNSUPPORTED command handler instead of the command that will be sent. So when we + // send the regular command, it will trigger the handling for unsupported/unrecognized commands. + commandToRegister = CommandNames.UNSUPPORTED; + runCommandAndVerifyOutput(commandHandler, "502 Unsupported"); + } + + // ------------------------------------------------------------------------- + // Internal Helper Methods + // ------------------------------------------------------------------------- + + /** + * Create and return a DefaultSession and define the specified CommandHandler. Also, save the + * StubSocket being used in the stubSocket attribute. + * + * @param commandHandler - define this CommandHandler within the commandHandlerMap + * @return the DefaultSession + */ + private DefaultSession createDefaultSession(CommandHandler commandHandler) { + stubSocket = createTestSocket(COMMAND.getName()); + commandHandlerMap.put(commandToRegister, commandHandler); + initializeConnectCommandHandler(); + return new DefaultSession(stubSocket, commandHandlerMap); + } + + private void initializeConnectCommandHandler() { + ConnectCommandHandler connectCommandHandler = new ConnectCommandHandler(); + + ResourceBundle replyTextBundle = new ListResourceBundle() { + protected Object[][] getContents() { + return new Object[][]{ + {"220", "Reply for 220"}, + }; + } + }; + connectCommandHandler.setReplyTextBundle(replyTextBundle); + commandHandlerMap.put(CommandNames.CONNECT, connectCommandHandler); + } + + /** + * Create and return a StubSocket that reads from an InputStream with the specified contents and + * writes to the predefined outputStrean ByteArrayOutputStream. + * + * @param inputStreamContents - the contents of the input stream + * @return the StubSocket + */ + private StubSocket createTestSocket(String inputStreamContents) { + InputStream inputStream = new ByteArrayInputStream(inputStreamContents.getBytes()); + return new StubSocket(DEFAULT_HOST, inputStream, outputStream); + } + + /** + * Run the command represented by the CommandHandler and verify that the session output from the + * control socket contains the expected output text. + * + * @param commandHandler - the CommandHandler to invoke + * @param expectedOutput - the text expected within the session output + * @throws InterruptedException - if the thread sleep is interrupted + */ + private void runCommandAndVerifyOutput(CommandHandler commandHandler, String expectedOutput) + throws InterruptedException { + session = createDefaultSession(commandHandler); + + Thread thread = new Thread(session); + thread.start(); + + for (int i = 0; !commandHandled && i < 10; i++) { + Thread.sleep(50L); + } + + session.close(); + thread.join(); + + assertEquals(true, commandHandled); + + String output = outputStream.toString(); + log("output=[" + output.trim() + "]"); + assertTrue(output.charAt(output.length() - 2) == '\r' && output.charAt(output.length() - 1) == '\n'); + assertTrue(output.indexOf(expectedOutput) != -1); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/socket/StubServerSocket.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/socket/StubServerSocket.java new file mode 100644 index 0000000..6401c71 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/socket/StubServerSocket.java @@ -0,0 +1,72 @@ +/* + * 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.test.core.socket; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; + +/** + * Test (fake) subclass of ServerSocket that performs no network access and allows setting the + * Socket returned by accept(), and the local port for the ServerSocket. + * + * @author Chris Mair + */ +public class StubServerSocket extends ServerSocket { + private int localPort; + private Socket socket; + + /** + * Construct a new instance with the specified local port. + * @param localPort - the local port to be returned from getLocalPort() + * @throws IOException - if an error occurs + */ + public StubServerSocket(int localPort) throws IOException { + this(localPort, null); + } + + /** + * Construct a new instance with specified local port and accept() socket. + * @param localPort - the local port to be returned from getLocalPort() + * @param socket - the socket to be returned from accept(); if null, then accept() throws SocketTimeoutException. + * @throws IOException - if an error occurs + */ + public StubServerSocket(int localPort, Socket socket) throws IOException { + super(0); + this.localPort = localPort; + this.socket = socket; + } + + /** + * Return the predefined local port + * @see ServerSocket#getLocalPort() + */ + public int getLocalPort() { + return localPort; + } + + /** + * If a socket was specified on the constructor, then return that; otherwise, throw a SocketTimeoutException. + * @see ServerSocket#accept() + */ + public Socket accept() throws IOException { + if (socket != null) { + return socket; + } + throw new SocketTimeoutException(); + } +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/socket/StubServerSocketFactory.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/socket/StubServerSocketFactory.java new file mode 100644 index 0000000..31668e0 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/socket/StubServerSocketFactory.java @@ -0,0 +1,47 @@ +/* + * 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.test.core.socket; + +import java.io.IOException; +import java.net.ServerSocket; +import org.xbib.files.ftp.mock.core.socket.ServerSocketFactory; + +/** + * Test-only implementation of ServerSocketFactory. It always returns the predefined + * ServerSocket instance specified on the constructor. + * + * @author Chris Mair + */ +public class StubServerSocketFactory implements ServerSocketFactory { + private StubServerSocket stubServerSocket; + + /** + * Construct a new factory instance that always returns the specified + * ServerSocket instance. + * @param serverSocket - the ServerSocket instance to be returned by this factory + */ + public StubServerSocketFactory(StubServerSocket serverSocket) { + this.stubServerSocket = serverSocket; + } + + /** + * Return the predefined ServerSocket instance. + * @see ServerSocketFactory#createServerSocket(int) + */ + public ServerSocket createServerSocket(int port) throws IOException { + return stubServerSocket; + } +} \ No newline at end of file diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/socket/StubSocket.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/socket/StubSocket.java new file mode 100644 index 0000000..05ed875 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/socket/StubSocket.java @@ -0,0 +1,99 @@ +/* + * 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.test.core.socket; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; + +/** + * Test (fake) subclass of Socket that performs no network access and allows setting the + * inputStream and OutputStream for the socket. + * + * @author Chris Mair + */ +public final class StubSocket extends Socket { + + private InetAddress inetAddress; + private InetAddress localAddress; + private InputStream inputStream; + private OutputStream outputStream; + + /** + * Construct a new instance using the specified InputStream and OutputStream + * @param inputStream - the InputStream to use + * @param outputStream - the OutputStream to use + */ + public StubSocket(InputStream inputStream, OutputStream outputStream) { + this(null, inputStream, outputStream); + } + + /** + * Construct a new instance using the specified host, InputStream and OutputStream + * @param inetAddress - the InetAddress for this socket + * @param inputStream - the InputStream to use + * @param outputStream - the OutputStream to use + */ + public StubSocket(InetAddress inetAddress, InputStream inputStream, OutputStream outputStream) { + this.inetAddress = inetAddress; + this.inputStream = inputStream; + this.outputStream = outputStream; + } + + /** + * Override the superclass implementation. If the local inetAddress is not null, + * return that. Otherwise return super.getInetAddress(). + * @see Socket#getInetAddress() + */ + public InetAddress getInetAddress() { + return (inetAddress != null) ? inetAddress : super.getInetAddress(); + } + + /** + * Override the superclass implementation. If the local localAddress is not + * null, return that. Otherwise return super.getLocalAddress(); + * @see Socket#getLocalAddress() + */ + public InetAddress getLocalAddress() { + return (localAddress != null) ? localAddress : super.getLocalAddress(); + } + + /** + * Override the superclass implementation to provide the predefined InputStream + * @see Socket#getInputStream() + */ + public InputStream getInputStream() throws IOException { + return inputStream; + } + + /** + * Override the superclass implementation to provide the predefined OutputStream + * @see Socket#getOutputStream() + */ + public OutputStream getOutputStream() throws IOException { + return outputStream; + } + + //------------------------------------------------------------------------- + // Test-specific helper methods + //------------------------------------------------------------------------- + + public void _setLocalAddress(InetAddress localAddress) { + this.localAddress = localAddress; + } +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/socket/StubSocketFactory.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/socket/StubSocketFactory.java new file mode 100644 index 0000000..f1e1266 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/socket/StubSocketFactory.java @@ -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.test.core.socket; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import org.xbib.files.ftp.mock.core.socket.SocketFactory; + +/** + * Test-only implementation of SocketFactory. It always returns the predefined + * StubSocket instance specified on the constructor. It allows direct access to the + * requested host address and port number. + * + * @author Chris Mair + */ +public class StubSocketFactory implements SocketFactory { + private StubSocket stubSocket; + public int requestedDataPort; + public InetAddress requestedHost; + + /** + * Create a new instance that always returns the specified StubSocket instance. + * @param stubSocket - the StubSocket to be returned by this factory + */ + public StubSocketFactory(StubSocket stubSocket) { + this.stubSocket = stubSocket; + } + + /** + * Return the predefined StubSocket instance + * @see SocketFactory#createSocket(InetAddress, int) + */ + public Socket createSocket(InetAddress host, int port) throws IOException { + this.requestedHost = host; + this.requestedDataPort = port; + return stubSocket; + } +} \ No newline at end of file diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/util/AssertTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/util/AssertTest.java new file mode 100644 index 0000000..2274630 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/core/util/AssertTest.java @@ -0,0 +1,124 @@ +/* + * 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.test.core.util; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.util.Assert; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.test.AbstractTestCase; + +import java.util.*; + +/** + * Tests for the Assert class + * + * @author Chris Mair + */ +class AssertTest extends AbstractTestCase { + + private static final String MESSAGE = "exception message"; + + @Test + void testAssertNull() { + Assert.isNull(null, MESSAGE); + + Throwable t = assertThrows(AssertFailedException.class, () -> Assert.isNull("OK", MESSAGE)); + assertExceptionMessageContains(t, MESSAGE); + } + + @Test + void testAssertNotNull() { + Assert.notNull("OK", MESSAGE); + + Throwable t = assertThrows(AssertFailedException.class, () -> Assert.notNull(null, MESSAGE)); + assertExceptionMessageContains(t, MESSAGE); + } + + @Test + void testAssertTrue() { + Assert.isTrue(true, MESSAGE); + + Throwable t = assertThrows(AssertFailedException.class, () -> Assert.isTrue(false, MESSAGE)); + assertExceptionMessageContains(t, MESSAGE); + } + + @Test + void testAssertFalse() { + Assert.isFalse(false, MESSAGE); + + Throwable t = assertThrows(AssertFailedException.class, () -> Assert.isFalse(true, MESSAGE)); + assertExceptionMessageContains(t, MESSAGE); + } + + @Test + void testAssertNotNullOrEmpty_Collection() { + final Collection COLLECTION = Collections.singletonList("item"); + Assert.notNullOrEmpty(COLLECTION, MESSAGE); + + Throwable t = assertThrows(AssertFailedException.class, () -> Assert.notNullOrEmpty((Collection) null, MESSAGE)); + assertExceptionMessageContains(t, MESSAGE); + + t = assertThrows(AssertFailedException.class, () -> Assert.notNullOrEmpty(new ArrayList(), MESSAGE)); + assertExceptionMessageContains(t, MESSAGE); + } + + @Test + void testAssertNotNullOrEmpty_Map() { + final Map MAP = Collections.singletonMap("key", "value"); + Assert.notNullOrEmpty(MAP, MESSAGE); + + Throwable t = assertThrows(AssertFailedException.class, () -> Assert.notNullOrEmpty((Map) null, MESSAGE)); + assertExceptionMessageContains(t, MESSAGE); + + t = assertThrows(AssertFailedException.class, () -> Assert.notNullOrEmpty(new HashMap(), MESSAGE)); + assertExceptionMessageContains(t, MESSAGE); + } + + @Test + void testAssertNotNullOrEmpty_array() { + final Object[] ARRAY = {"1", "2"}; + Assert.notNullOrEmpty(ARRAY, MESSAGE); + + Throwable t = assertThrows(AssertFailedException.class, () -> Assert.notNullOrEmpty((Object[]) null, MESSAGE)); + assertExceptionMessageContains(t, MESSAGE); + + t = assertThrows(AssertFailedException.class, () -> Assert.notNullOrEmpty(new String[]{}, MESSAGE)); + assertExceptionMessageContains(t, MESSAGE); + } + + @Test + void testAssertNotNullOrEmpty_String() { + Assert.notNullOrEmpty("OK", MESSAGE); + + Throwable t = assertThrows(AssertFailedException.class, () -> Assert.notNullOrEmpty((String) null, MESSAGE)); + assertExceptionMessageContains(t, MESSAGE); + + t = assertThrows(AssertFailedException.class, () -> Assert.notNullOrEmpty("", MESSAGE)); + assertExceptionMessageContains(t, MESSAGE); + } + + //------------------------------------------------------------------------- + // Helper Methods + //------------------------------------------------------------------------- + + private void assertExceptionMessageContains(Throwable exception, String text) { + String message = exception.getMessage(); + assertTrue(message.indexOf(text) != -1); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/example/RemoteFileTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/example/RemoteFileTest.java new file mode 100644 index 0000000..a9c50a0 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/example/RemoteFileTest.java @@ -0,0 +1,82 @@ +/* + * 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.test.example; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.fake.FakeFtpServer; +import org.xbib.files.ftp.mock.fake.UserAccount; +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.UnixFakeFileSystem; +import org.xbib.files.ftp.mock.test.stub.example.RemoteFile; +import org.xbib.files.ftp.mock.test.AbstractTestCase; +import org.xbib.files.ftp.mock.test.IntegrationTest; + +import java.io.IOException; + +/** + * Example test using FakeFtpServer, with programmatic configuration. + */ +class RemoteFileTest extends AbstractTestCase implements IntegrationTest { + + private static final String HOME_DIR = "/"; + private static final String FILE = "/dir/sample.txt"; + private static final String CONTENTS = "abcdef 1234567890"; + + private RemoteFile remoteFile; + private FakeFtpServer fakeFtpServer; + + @Test + void testReadFile() throws Exception { + String contents = remoteFile.readFile(FILE); + assertEquals(CONTENTS, contents); + } + + @Test + void testReadFileThrowsException() { + assertThrows(IOException.class, () -> remoteFile.readFile("NoSuchFile.txt")); + } + + @BeforeEach + void before() throws Exception { + fakeFtpServer = new FakeFtpServer(); + fakeFtpServer.setServerControlPort(0); // use any free port + + FileSystem fileSystem = new UnixFakeFileSystem(); + fileSystem.add(new FileEntry(FILE, CONTENTS)); + fakeFtpServer.setFileSystem(fileSystem); + + UserAccount userAccount = new UserAccount(RemoteFile.USERNAME, RemoteFile.PASSWORD, HOME_DIR); + fakeFtpServer.addUserAccount(userAccount); + + fakeFtpServer.start(); + int port = fakeFtpServer.getServerControlPort(); + + remoteFile = new RemoteFile(); + remoteFile.setServer("localhost"); + remoteFile.setPort(port); + } + + @AfterEach + void after() throws Exception { + fakeFtpServer.stop(); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/example/SimpleUnixFakeFtpServerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/example/SimpleUnixFakeFtpServerTest.java new file mode 100644 index 0000000..0daa778 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/example/SimpleUnixFakeFtpServerTest.java @@ -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.test.example; + +import org.junit.jupiter.api.Test; +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.UnixFakeFileSystem; +import org.xbib.files.ftp.mock.test.AbstractTestCase; +import org.xbib.files.ftp.mock.test.IntegrationTest; + +/** + * Example code illustrating how to programmatically configure a FakeFtpServer with a (simulated) Unix + * filesystem. + */ +class SimpleUnixFakeFtpServerTest extends AbstractTestCase implements IntegrationTest { + + @Test + void testConfigureAndStart() throws Exception { + FakeFtpServer fakeFtpServer = new FakeFtpServer(); + fakeFtpServer.setServerControlPort(9981); + fakeFtpServer.addUserAccount(new UserAccount("user", "password", "c:\\data")); + + FileSystem fileSystem = new UnixFakeFileSystem(); + fileSystem.add(new DirectoryEntry("/data")); + fileSystem.add(new FileEntry("/data/file1.txt", "abcdef 1234567890")); + fileSystem.add(new FileEntry("/data/run.exe")); + fakeFtpServer.setFileSystem(fileSystem); + + fakeFtpServer.start(); + + fakeFtpServer.stop(); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/example/SimpleWindowsFakeFtpServerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/example/SimpleWindowsFakeFtpServerTest.java new file mode 100644 index 0000000..3da345b --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/example/SimpleWindowsFakeFtpServerTest.java @@ -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.test.example; + +import org.junit.jupiter.api.Test; +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.WindowsFakeFileSystem; +import org.xbib.files.ftp.mock.test.AbstractTestCase; +import org.xbib.files.ftp.mock.test.IntegrationTest; + +/** + * Example code illustrating how to programmatically configure a FakeFtpServer with a (simulated) Windows + * filesystem. + */ +class SimpleWindowsFakeFtpServerTest extends AbstractTestCase implements IntegrationTest { + + @Test + void testConfigureAndStart() throws Exception { + FakeFtpServer fakeFtpServer = new FakeFtpServer(); + fakeFtpServer.setServerControlPort(9981); + fakeFtpServer.addUserAccount(new UserAccount("user", "password", "c:\\data")); + + FileSystem fileSystem = new WindowsFakeFileSystem(); + 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); + + fakeFtpServer.start(); + + fakeFtpServer.stop(); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/example/WindowsFakeFileSystemPermissionsTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/example/WindowsFakeFileSystemPermissionsTest.java new file mode 100644 index 0000000..d0fa2c1 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/example/WindowsFakeFileSystemPermissionsTest.java @@ -0,0 +1,73 @@ +/* + * 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.test.example; + +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.fake.FakeFtpServer; +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.Permissions; +import org.xbib.files.ftp.mock.fake.filesystem.WindowsFakeFileSystem; +import org.xbib.files.ftp.mock.test.AbstractTestCase; +import org.xbib.files.ftp.mock.test.IntegrationTest; + +/** + * Example code illustrating how to programmatically configure a FakeFtpServer with a (simulated) Windows + * filesystem, and including file/directory permissions. + */ +class WindowsFakeFileSystemPermissionsTest extends AbstractTestCase implements IntegrationTest { + + @Test + void testFilesystemWithPermissions() throws Exception { + final String USER1 = "joe"; + final String USER2 = "mary"; + final String GROUP = "dev"; + final String CONTENTS = "abcdef 1234567890"; + + FileSystem fileSystem = new WindowsFakeFileSystem(); + DirectoryEntry directoryEntry1 = new DirectoryEntry("c:\\"); + directoryEntry1.setPermissions(new Permissions("rwxrwx---")); + directoryEntry1.setOwner(USER1); + directoryEntry1.setGroup(GROUP); + + DirectoryEntry directoryEntry2 = new DirectoryEntry("c:\\data"); + directoryEntry2.setPermissions(Permissions.ALL); + directoryEntry2.setOwner(USER1); + directoryEntry2.setGroup(GROUP); + + FileEntry fileEntry1 = new FileEntry("c:\\data\\file1.txt", CONTENTS); + fileEntry1.setPermissionsFromString("rw-rw-rw-"); + fileEntry1.setOwner(USER1); + fileEntry1.setGroup(GROUP); + + FileEntry fileEntry2 = new FileEntry("c:\\data\\run.exe"); + fileEntry2.setPermissionsFromString("rwxrwx---"); + fileEntry2.setOwner(USER2); + fileEntry2.setGroup(GROUP); + + fileSystem.add(directoryEntry1); + fileSystem.add(directoryEntry2); + fileSystem.add(fileEntry1); + fileSystem.add(fileEntry2); + + FakeFtpServer fakeFtpServer = new FakeFtpServer(); + fakeFtpServer.setFileSystem(fileSystem); + + log(fileSystem.toString()); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/StubFtpServerIntegrationTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/StubFtpServerIntegrationTest.java new file mode 100644 index 0000000..acedfee --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/StubFtpServerIntegrationTest.java @@ -0,0 +1,594 @@ +/* + * 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.test.stub; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.command.SimpleCompositeCommandHandler; +import org.xbib.files.ftp.mock.core.command.StaticReplyCommandHandler; +import org.xbib.files.ftp.mock.stub.StubFtpServer; +import org.xbib.files.ftp.mock.stub.command.AppeCommandHandler; +import org.xbib.files.ftp.mock.stub.command.CwdCommandHandler; +import org.xbib.files.ftp.mock.stub.command.HelpCommandHandler; +import org.xbib.files.ftp.mock.stub.command.ListCommandHandler; +import org.xbib.files.ftp.mock.stub.command.NlstCommandHandler; +import org.xbib.files.ftp.mock.stub.command.PwdCommandHandler; +import org.xbib.files.ftp.mock.stub.command.RetrCommandHandler; +import org.xbib.files.ftp.mock.stub.command.StatCommandHandler; +import org.xbib.files.ftp.mock.stub.command.StorCommandHandler; +import org.xbib.files.ftp.mock.stub.command.StouCommandHandler; +import org.xbib.files.ftp.mock.test.AbstractTestCase; +import org.xbib.files.ftp.mock.test.IntegrationTest; +import org.xbib.files.ftp.mock.test.PortTestUtil; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.xbib.io.ftp.client.FTP; +import org.xbib.io.ftp.client.FTPClient; +import org.xbib.io.ftp.client.FTPFile; + +/** + * Tests for StubFtpServer using the Apache Jakarta Commons Net FTP client. + * + * @author Chris Mair + */ +class StubFtpServerIntegrationTest extends AbstractTestCase implements IntegrationTest { + + private static final String SERVER = "localhost"; + private static final String USERNAME = "user123"; + private static final String PASSWORD = "password"; + private static final String FILENAME = "abc.txt"; + private static final String ASCII_CONTENTS = "abcdef\tghijklmnopqr"; + private static final byte[] BINARY_CONTENTS = new byte[256]; + + private StubFtpServer stubFtpServer; + private FTPClient ftpClient; + private RetrCommandHandler retrCommandHandler; + private StorCommandHandler storCommandHandler; + + //------------------------------------------------------------------------- + // Tests + //------------------------------------------------------------------------- + + @Test + void testLogin() throws Exception { + // Connect + log("Conecting to " + SERVER); + ftpClientConnect(); + verifyReplyCode("connect", 220); + + // Login + String userAndPassword = USERNAME + "/" + PASSWORD; + log("Logging in as " + userAndPassword); + boolean success = ftpClient.login(USERNAME, PASSWORD); + assertTrue(success); + verifyReplyCode("login with " + userAndPassword, 230); + + assertTrue(stubFtpServer.isStarted()); + assertFalse(stubFtpServer.isShutdown()); + + // Quit + log("Quit"); + ftpClient.quit(); + verifyReplyCode("quit", 221); + } + + @Test + void testAcct() throws Exception { + ftpClientConnect(); + + // ACCT + int replyCode = ftpClient.acct("123456"); + assertEquals(230, replyCode); + } + + @Test + void testStop_NoSessionEverStarted() { + log("Testing a stop() when no session has ever been started"); + } + + @Test + void testHelp() throws Exception { + // Modify HELP CommandHandler to return a predefined help message + final String HELP = "help message"; + HelpCommandHandler helpCommandHandler = (HelpCommandHandler) stubFtpServer.getCommandHandler(CommandNames.HELP); + helpCommandHandler.setHelpMessage(HELP); + + ftpClientConnect(); + + // HELP + String help = ftpClient.listHelp(); + assertTrue(help.indexOf(HELP) != -1); + verifyReplyCode("listHelp", 214); + } + + @Test + void testList() throws Exception { + ftpClientConnect(); + + // Set directory listing + ListCommandHandler listCommandHandler = (ListCommandHandler) stubFtpServer.getCommandHandler(CommandNames.LIST); + listCommandHandler.setDirectoryListing("11-09-01 12:30PM 406348 File2350.log\n" + + "11-01-01 1:30PM archive"); + + // LIST + FTPFile[] files = ftpClient.listFiles(); + assertEquals(2, files.length); + verifyFTPFile(files[0], FTPFile.FILE_TYPE, "File2350.log", 406348L); + verifyFTPFile(files[1], FTPFile.DIRECTORY_TYPE, "archive", 0L); + verifyReplyCode("list", 226); + } + + @Test + void testList_PassiveMode() throws Exception { + ftpClientConnect(); + + ftpClient.enterLocalPassiveMode(); + + // Set directory listing + ListCommandHandler listCommandHandler = (ListCommandHandler) stubFtpServer.getCommandHandler(CommandNames.LIST); + listCommandHandler.setDirectoryListing("11-09-01 12:30PM 406348 File2350.log"); + + // LIST + FTPFile[] files = ftpClient.listFiles(); + assertEquals(1, files.length); + verifyReplyCode("list", 226); + } + + @Test + void testNlst() throws Exception { + ftpClientConnect(); + + // Set directory listing + NlstCommandHandler nlstCommandHandler = (NlstCommandHandler) stubFtpServer.getCommandHandler(CommandNames.NLST); + nlstCommandHandler.setDirectoryListing("File1.txt\nfile2.data"); + + // NLST + String[] filenames = ftpClient.listNames(); + assertEquals(2, filenames.length); + assertEquals(filenames[0], "File1.txt", "filenames[0]"); + assertEquals(filenames[1], "file2.data", "filenames[1]"); + verifyReplyCode("listNames", 226); + } + + @Test + void testPwd() throws Exception { + // Modify PWD CommandHandler to return a predefined directory + final String DIR = "some/dir"; + PwdCommandHandler pwdCommandHandler = (PwdCommandHandler) stubFtpServer.getCommandHandler(CommandNames.PWD); + pwdCommandHandler.setDirectory(DIR); + + ftpClientConnect(); + + // PWD + String dir = ftpClient.printWorkingDirectory(); + assertEquals(DIR, dir); + verifyReplyCode("printWorkingDirectory", 257); + } + + @Test + void testStat() throws Exception { + // Modify Stat CommandHandler to return predefined text + final String STATUS = "some information 123"; + StatCommandHandler statCommandHandler = (StatCommandHandler) stubFtpServer.getCommandHandler(CommandNames.STAT); + statCommandHandler.setStatus(STATUS); + + ftpClientConnect(); + + // STAT + String status = ftpClient.getStatus(); + assertEquals("211 " + STATUS + ".", status.trim()); + verifyReplyCode("getStatus", 211); + } + + @Test + void testStat_MultilineReplyText() throws Exception { + // Modify Stat CommandHandler to return predefined text + final String STATUS = "System name: abc.def\r\nVersion 3.5.7\r\nNumber of failed logins: 2"; + final String FORMATTED_REPLY_STATUS = "211-System name: abc.def\r\nVersion 3.5.7\r\n211 Number of failed logins: 2."; + StatCommandHandler statCommandHandler = (StatCommandHandler) stubFtpServer.getCommandHandler(CommandNames.STAT); + statCommandHandler.setStatus(STATUS); + + ftpClientConnect(); + + // STAT + String status = ftpClient.getStatus(); + assertEquals(FORMATTED_REPLY_STATUS, status.trim()); + verifyReplyCode("getStatus", 211); + } + + @Test + void testCwd() throws Exception { + // Connect + log("Conecting to " + SERVER); + ftpClientConnect(); + verifyReplyCode("connect", 220); + + // CWD + boolean success = ftpClient.changeWorkingDirectory("dir1/dir2"); + assertTrue(success); + verifyReplyCode("changeWorkingDirectory", 250); + } + + @Test + void testCwd_Error() throws Exception { + // Override CWD CommandHandler to return error reply code + final int REPLY_CODE = 500; + StaticReplyCommandHandler cwdCommandHandler = new StaticReplyCommandHandler(REPLY_CODE); + stubFtpServer.setCommandHandler("CWD", cwdCommandHandler); + + ftpClientConnect(); + + // CWD + boolean success = ftpClient.changeWorkingDirectory("dir1/dir2"); + assertFalse(success); + verifyReplyCode("changeWorkingDirectory", REPLY_CODE); + } + + @Test + void testCdup() throws Exception { + ftpClientConnect(); + + // CDUP + boolean success = ftpClient.changeToParentDirectory(); + assertTrue(success); + verifyReplyCode("changeToParentDirectory", 200); + } + + @Test + void testDele() throws Exception { + ftpClientConnect(); + + // DELE + boolean success = ftpClient.deleteFile(FILENAME); + assertTrue(success); + verifyReplyCode("deleteFile", 250); + } + + @Test + void testEprt() { + log("Skipping..."); +// ftpClientConnect(); +// ftpClient.sendCommand("EPRT", "|2|1080::8:800:200C:417A|5282|"); +// verifyReplyCode("EPRT", 200); + } + + @Test + void testEpsv() throws Exception { + ftpClientConnect(); + ftpClient.sendCommand("EPSV"); + verifyReplyCode("EPSV", 229); + } + + @Test + void testFeat_UseStaticReplyCommandHandler() throws IOException { + // The FEAT command is not supported out of the box + final String FEAT_TEXT = "Extensions supported:\r\n" + + "MLST size*;create;modify*;perm;media-type\r\n" + + "SIZE\r\n" + + "COMPRESSION\r\n" + + "END"; + StaticReplyCommandHandler featCommandHandler = new StaticReplyCommandHandler(211, FEAT_TEXT); + stubFtpServer.setCommandHandler("FEAT", featCommandHandler); + + ftpClientConnect(); + assertEquals(ftpClient.sendCommand("FEAT"), 211); + log(ftpClient.getReplyString()); + } + + @Test + void testMkd() throws Exception { + ftpClientConnect(); + + // MKD + boolean success = ftpClient.makeDirectory("dir1/dir2"); + assertTrue(success); + verifyReplyCode("makeDirectory", 257); + } + + @Test + void testNoop() throws Exception { + ftpClientConnect(); + + // NOOP + boolean success = ftpClient.sendNoOp(); + assertTrue(success); + verifyReplyCode("NOOP", 200); + } + + @Test + void testRest() throws Exception { + ftpClientConnect(); + + // REST + int replyCode = ftpClient.rest("marker"); + assertEquals(350, replyCode); + } + + @Test + void testRmd() throws Exception { + ftpClientConnect(); + + // RMD + boolean success = ftpClient.removeDirectory("dir1/dir2"); + assertTrue(success); + verifyReplyCode("removeDirectory", 250); + } + + @Test + void testRename() throws Exception { + ftpClientConnect(); + + // Rename (RNFR, RNTO) + boolean success = ftpClient.rename(FILENAME, "new_" + FILENAME); + assertTrue(success); + verifyReplyCode("rename", 250); + } + + @Test + void testAllo() throws Exception { + ftpClientConnect(); + + // ALLO + assertTrue(ftpClient.allocate(1024)); + assertTrue(ftpClient.allocate(1024, 64)); + } + + @Test + void testTransferAsciiFile() throws Exception { + retrCommandHandler.setFileContents(ASCII_CONTENTS); + + ftpClientConnect(); + + // Get File + log("Get File for remotePath [" + FILENAME + "]"); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + assertTrue(ftpClient.retrieveFile(FILENAME, outputStream)); + log("File contents=[" + outputStream.toString()); + assertEquals(ASCII_CONTENTS, outputStream.toString()); + + // Put File + log("Put File for local path [" + FILENAME + "]"); + ByteArrayInputStream inputStream = new ByteArrayInputStream(ASCII_CONTENTS.getBytes()); + assertTrue(ftpClient.storeFile(FILENAME, inputStream)); + InvocationRecord invocationRecord = storCommandHandler.getInvocation(0); + byte[] contents = (byte[]) invocationRecord.getObject(StorCommandHandler.FILE_CONTENTS_KEY); + log("File contents=[" + contents + "]"); + assertArrayEquals(ASCII_CONTENTS.getBytes(), contents); + } + + @Test + void testTransferBinaryFiles() throws Exception { + retrCommandHandler.setFileContents(BINARY_CONTENTS); + + ftpClientConnect(); + ftpClient.setFileType(FTP.BINARY_FILE_TYPE); + + // Get File + log("Get File for remotePath [" + FILENAME + "]"); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + assertTrue(ftpClient.retrieveFile(FILENAME, outputStream)); + log("GET File length=" + outputStream.size()); + assertArrayEquals(BINARY_CONTENTS, outputStream.toByteArray()); + + // Put File + log("Put File for local path [" + FILENAME + "]"); + ByteArrayInputStream inputStream = new ByteArrayInputStream(BINARY_CONTENTS); + assertTrue(ftpClient.storeFile(FILENAME, inputStream)); + InvocationRecord invocationRecord = storCommandHandler.getInvocation(0); + byte[] contents = (byte[]) invocationRecord.getObject(StorCommandHandler.FILE_CONTENTS_KEY); + log("PUT File length=" + contents.length); + assertArrayEquals(BINARY_CONTENTS, contents); + } + + @Test + void testStou() throws Exception { + StouCommandHandler stouCommandHandler = (StouCommandHandler) stubFtpServer.getCommandHandler(CommandNames.STOU); + stouCommandHandler.setFilename(FILENAME); + + ftpClientConnect(); + + // Stor a File (STOU) + ByteArrayInputStream inputStream = new ByteArrayInputStream(ASCII_CONTENTS.getBytes()); + assertTrue(ftpClient.storeUniqueFile(FILENAME, inputStream)); + InvocationRecord invocationRecord = stouCommandHandler.getInvocation(0); + byte[] contents = (byte[]) invocationRecord.getObject(StorCommandHandler.FILE_CONTENTS_KEY); + log("File contents=[" + contents + "]"); + assertArrayEquals(ASCII_CONTENTS.getBytes(), contents); + } + + @Test + void testAppe() throws Exception { + AppeCommandHandler appeCommandHandler = (AppeCommandHandler) stubFtpServer.getCommandHandler(CommandNames.APPE); + + ftpClientConnect(); + + // Append a File (APPE) + ByteArrayInputStream inputStream = new ByteArrayInputStream(ASCII_CONTENTS.getBytes()); + assertTrue(ftpClient.appendFile(FILENAME, inputStream)); + InvocationRecord invocationRecord = appeCommandHandler.getInvocation(0); + byte[] contents = (byte[]) invocationRecord.getObject(AppeCommandHandler.FILE_CONTENTS_KEY); + log("File contents=[" + contents + "]"); + assertArrayEquals(ASCII_CONTENTS.getBytes(), contents); + } + + @Test + void testAbor() throws Exception { + ftpClientConnect(); + + // ABOR + assertTrue(ftpClient.abort()); + } + + @Test + void testPasv() throws Exception { + ftpClientConnect(); + + // PASV + ftpClient.enterLocalPassiveMode(); + // no reply code; the PASV command is sent only when the data connection is opened + } + + @Test + void testMode() throws Exception { + ftpClientConnect(); + + // MODE + boolean success = ftpClient.setFileTransferMode(FTP.STREAM_TRANSFER_MODE); + assertTrue(success); + verifyReplyCode("setFileTransferMode", 200); + } + + @Test + void testStru() throws Exception { + ftpClientConnect(); + + // STRU + boolean success = ftpClient.setFileStructure(FTP.FILE_STRUCTURE); + assertTrue(success); + verifyReplyCode("setFileStructure", 200); + } + + @Test + void testSimpleCompositeCommandHandler() throws Exception { + // Replace CWD CommandHandler with a SimpleCompositeCommandHandler + CommandHandler commandHandler1 = new StaticReplyCommandHandler(500); + CommandHandler commandHandler2 = new CwdCommandHandler(); + SimpleCompositeCommandHandler simpleCompositeCommandHandler = new SimpleCompositeCommandHandler(); + simpleCompositeCommandHandler.addCommandHandler(commandHandler1); + simpleCompositeCommandHandler.addCommandHandler(commandHandler2); + stubFtpServer.setCommandHandler("CWD", simpleCompositeCommandHandler); + + // Connect + ftpClientConnect(); + + // CWD + assertFalse(ftpClient.changeWorkingDirectory("dir1/dir2")); + assertTrue(ftpClient.changeWorkingDirectory("dir1/dir2")); + } + + @Test + void testSite() throws Exception { + ftpClientConnect(); + + // SITE + int replyCode = ftpClient.site("parameters,1,2,3"); + assertEquals(200, replyCode); + } + + @Test + void testSmnt() throws Exception { + ftpClientConnect(); + + // SMNT + assertTrue(ftpClient.structureMount("dir1/dir2")); + verifyReplyCode("structureMount", 250); + } + + @Test + void testRein() throws Exception { + ftpClientConnect(); + + // REIN + assertEquals(220, ftpClient.rein()); + } + + @Test + void testCommandNamesInLowerOrMixedCase() throws Exception { + ftpClientConnect(); + + assertEquals(220, ftpClient.sendCommand("rein")); + assertEquals(220, ftpClient.sendCommand("rEIn")); + assertEquals(220, ftpClient.sendCommand("reiN")); + assertEquals(220, ftpClient.sendCommand("Rein")); + } + + @Test + void testUnrecognizedCommand() throws Exception { + ftpClientConnect(); + + assertEquals(502, ftpClient.sendCommand("XXXX")); + } + + // ------------------------------------------------------------------------- + // Test setup and tear-down + // ------------------------------------------------------------------------- + + @BeforeEach + void setUp() { + for (int i = 0; i < BINARY_CONTENTS.length; i++) { + BINARY_CONTENTS[i] = (byte) i; + } + + stubFtpServer = new StubFtpServer(); + stubFtpServer.setServerControlPort(PortTestUtil.getFtpServerControlPort()); + stubFtpServer.start(); + ftpClient = new FTPClient(); + retrCommandHandler = (RetrCommandHandler) stubFtpServer.getCommandHandler(CommandNames.RETR); + storCommandHandler = (StorCommandHandler) stubFtpServer.getCommandHandler(CommandNames.STOR); + } + + @AfterEach + void tearDown() { + stubFtpServer.stop(); + } + + // ------------------------------------------------------------------------- + // Internal Helper Methods + // ------------------------------------------------------------------------- + + /** + * Connect to the server from the FTPClient + */ + private void ftpClientConnect() throws IOException { + ftpClient.connect(SERVER, PortTestUtil.getFtpServerControlPort()); + } + + /** + * Assert that the FtpClient reply code is equal to the expected value + * + * @param operation - the description of the operation performed; used in the error message + * @param expectedReplyCode - the expected FtpClient reply code + */ + private void verifyReplyCode(String operation, int expectedReplyCode) { + int replyCode = ftpClient.getReplyCode(); + log("Reply: operation=\"" + operation + "\" replyCode=" + replyCode); + assertEquals(expectedReplyCode, replyCode); + } + + /** + * Verify that the FTPFile has the specified properties + * + * @param ftpFile - the FTPFile to verify + * @param type - the expected file type + * @param name - the expected file name + * @param size - the expected file size (will be zero for a directory) + */ + private void verifyFTPFile(FTPFile ftpFile, int type, String name, long size) { + log(ftpFile.toString()); + assertEquals(type, ftpFile.getType()); + assertEquals(name, ftpFile.getName()); + assertEquals(size, ftpFile.getSize()); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/StubFtpServerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/StubFtpServerTest.java new file mode 100644 index 0000000..22ecb69 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/StubFtpServerTest.java @@ -0,0 +1,115 @@ +/* + * 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.test.stub; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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.InvocationRecord; +import org.xbib.files.ftp.mock.core.server.AbstractFtpServer; +import org.xbib.files.ftp.mock.core.session.Session; +import org.xbib.files.ftp.mock.stub.StubFtpServer; +import org.xbib.files.ftp.mock.stub.command.AbstractStubCommandHandler; +import org.xbib.files.ftp.mock.stub.command.CwdCommandHandler; +import org.xbib.files.ftp.mock.test.core.server.AbstractFtpServerTestCase; + +import java.util.ResourceBundle; + +/** + * Unit tests for StubFtpServer. Also see {@link StubFtpServer_StartTest} + * and {@link StubFtpServerIntegrationTest}. + * + * @author Chris Mair + */ +class StubFtpServerTest extends AbstractFtpServerTestCase { + + private StubFtpServer stubFtpServer; + private AbstractStubCommandHandler commandHandler; + private CommandHandler commandHandler_NoReplyTextBundle; + + //------------------------------------------------------------------------- + // Extra tests (Standard tests defined in superclass) + //------------------------------------------------------------------------- + + @Test + void testSetCommandHandler_NotReplyTextBundleAware() { + stubFtpServer.setCommandHandler("ZZZ", commandHandler_NoReplyTextBundle); + assertSame(commandHandler_NoReplyTextBundle, stubFtpServer.getCommandHandler("ZZZ")); + } + + @Test + void testSetCommandHandler_NullReplyTextBundle() { + stubFtpServer.setCommandHandler("ZZZ", commandHandler); + assertSame(commandHandler, stubFtpServer.getCommandHandler("ZZZ")); + assertSame(stubFtpServer.getReplyTextBundle(), commandHandler.getReplyTextBundle()); + } + + @Test + void testSetReplyTextBaseName() { + stubFtpServer.setReplyTextBaseName("SampleReplyText"); + CwdCommandHandler commandHandler = new CwdCommandHandler(); + + // The resource bundle is passed along to new CommandHandlers (if they don't already have one) + stubFtpServer.setCommandHandler("CWD", commandHandler); + ResourceBundle resourceBundle = commandHandler.getReplyTextBundle(); + assertEquals("Testing123", resourceBundle.getString("110")); + } + + //------------------------------------------------------------------------- + // Test setup + //------------------------------------------------------------------------- + + @BeforeEach + void setUp() { + stubFtpServer = (StubFtpServer) ftpServer; + + // Create a CommandHandler instance that also implements ResourceBundleAware + commandHandler = new AbstractStubCommandHandler() { + protected void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + } + }; + + // Create a CommandHandler instance that does NOT implement ResourceBundleAware + commandHandler_NoReplyTextBundle = new CommandHandler() { + public void handleCommand(Command command, Session session) throws Exception { + } + }; + } + + //------------------------------------------------------------------------- + // Abstract method implementations + //------------------------------------------------------------------------- + + protected AbstractFtpServer createFtpServer() { + return new StubFtpServer(); + } + + protected CommandHandler createCommandHandler() { + return new AbstractStubCommandHandler() { + protected void handleCommand(Command command, Session session, InvocationRecord invocationRecord) { + } + }; + } + + protected void verifyCommandHandlerInitialized(CommandHandler commandHandler) { + AbstractStubCommandHandler stubCommandHandler = (AbstractStubCommandHandler) commandHandler; + assertSame(stubFtpServer.getReplyTextBundle(), stubCommandHandler.getReplyTextBundle()); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/StubFtpServer_MultipleClientsIntegrationTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/StubFtpServer_MultipleClientsIntegrationTest.java new file mode 100644 index 0000000..ed99b53 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/StubFtpServer_MultipleClientsIntegrationTest.java @@ -0,0 +1,125 @@ +/* + * 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.test.stub; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.InvocationRecord; +import org.xbib.files.ftp.mock.core.session.Session; +import org.xbib.files.ftp.mock.stub.StubFtpServer; +import org.xbib.files.ftp.mock.stub.command.AbstractStubCommandHandler; +import org.xbib.files.ftp.mock.test.AbstractTestCase; +import org.xbib.files.ftp.mock.test.IntegrationTest; +import org.xbib.files.ftp.mock.test.PortTestUtil; +import org.xbib.io.ftp.client.FTPClient; + +/** + * StubFtpServer tests for multiple FTP clients using the Apache Jakarta Commons Net FTP client. + * + * @author Chris Mair + */ +class StubFtpServer_MultipleClientsIntegrationTest extends AbstractTestCase implements IntegrationTest { + + private static final String SERVER = "localhost"; + + // Custom CommandHandler for PWD so that we can verify unique session-specific responses. + // Send back the hashCode for the Session as the reply text. + private static class CustomPwdCommandHandler extends AbstractStubCommandHandler { + protected void handleCommand(Command command, Session session, InvocationRecord invocationRecord) throws Exception { + String replyText = quotes(Integer.toString(session.hashCode())); + sendReply(session, 257, null, replyText, null); + } + } + + private StubFtpServer stubFtpServer; + private FTPClient ftpClient1; + private FTPClient ftpClient2; + private FTPClient ftpClient3; + + @Test + void testMultipleClients() throws Exception { + + // Connect from client 1 + log("connect() to ftpClient1"); + ftpClient1.connect(SERVER, PortTestUtil.getFtpServerControlPort()); + String sessionId1 = ftpClient1.printWorkingDirectory(); + log("PWD(1) reply =[" + sessionId1 + "]"); + + // Connect from client 2 + log("connect() to ftpClient2"); + ftpClient2.connect(SERVER, PortTestUtil.getFtpServerControlPort()); + String sessionId2 = ftpClient2.printWorkingDirectory(); + log("PWD(2) reply =[" + sessionId2 + "]"); + + // Connect from client 3 + log("connect() to ftpClient3"); + ftpClient3.connect(SERVER, PortTestUtil.getFtpServerControlPort()); + String sessionId3 = ftpClient3.printWorkingDirectory(); + log("PWD(3) reply =[" + sessionId3 + "]"); + + // Make sure all session ids are unique + assertNotSame("sessionId1 vs sessionId2", sessionId1, sessionId2); + assertNotSame("sessionId2 vs sessionId3", sessionId2, sessionId3); + assertNotSame("sessionId1 vs sessionId3", sessionId1, sessionId3); + + // Now make sure that the replies from the existing sessions remain consistent + assertEquals(sessionId1, ftpClient1.printWorkingDirectory()); + assertEquals(sessionId2, ftpClient2.printWorkingDirectory()); + assertEquals(sessionId3, ftpClient3.printWorkingDirectory()); + } + + // ------------------------------------------------------------------------- + // Test setup and tear-down + // ------------------------------------------------------------------------- + + @BeforeEach + void setUp() { + stubFtpServer = new StubFtpServer(); + stubFtpServer.setServerControlPort(PortTestUtil.getFtpServerControlPort()); + stubFtpServer.setCommandHandler(CommandNames.PWD, new CustomPwdCommandHandler()); + stubFtpServer.start(); + + ftpClient1 = new FTPClient(); + ftpClient2 = new FTPClient(); + ftpClient3 = new FTPClient(); + + ftpClient1.setDefaultTimeout(1000); + ftpClient2.setDefaultTimeout(1000); + ftpClient3.setDefaultTimeout(1000); + } + + @AfterEach + protected void tearDown() throws Exception { + log("Cleaning up..."); + if (ftpClient1.isConnected()) { + ftpClient1.disconnect(); + } + if (ftpClient2.isConnected()) { + ftpClient2.disconnect(); + } + if (ftpClient3.isConnected()) { + ftpClient3.disconnect(); + } + + stubFtpServer.stop(); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/StubFtpServer_StartTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/StubFtpServer_StartTest.java new file mode 100644 index 0000000..83dc691 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/StubFtpServer_StartTest.java @@ -0,0 +1,37 @@ +/* + * 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.test.stub; + +import org.xbib.files.ftp.mock.core.server.AbstractFtpServer; +import org.xbib.files.ftp.mock.stub.StubFtpServer; +import org.xbib.files.ftp.mock.test.core.server.AbstractFtpServer_StartTestCase; + +/** + * Tests for StubFtpServer that require the StubFtpServer thread to be started. + * + * @author Chris Mair + */ +class StubFtpServer_StartTest extends AbstractFtpServer_StartTestCase { + + //------------------------------------------------------------------------- + // Abstract method implementations + //------------------------------------------------------------------------- + + protected AbstractFtpServer createFtpServer() { + return new StubFtpServer(); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AborCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AborCommandHandlerTest.java new file mode 100644 index 0000000..2241961 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AborCommandHandlerTest.java @@ -0,0 +1,54 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.AborCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the AborCommandHandler class + * + * @author Chris Mair + */ +class AborCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private AborCommandHandler commandHandler; + + @Test + void testHandleCommand() throws Exception { + final Command COMMAND = new Command(CommandNames.ABOR, EMPTY); + + commandHandler.handleCommand(COMMAND, session); + + verify(session).sendReply(ReplyCodes.ABOR_OK, replyTextFor(ReplyCodes.ABOR_OK)); + verifyNumberOfInvocations(commandHandler, 1); + verifyNoDataElements(commandHandler.getInvocation(0)); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new AborCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AbstractStubDataCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AbstractStubDataCommandHandlerTest.java new file mode 100644 index 0000000..2c08e81 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AbstractStubDataCommandHandlerTest.java @@ -0,0 +1,144 @@ +/* + * 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.test.stub.command; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.InvocationRecord; +import org.xbib.files.ftp.mock.core.session.Session; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.stub.command.AbstractStubDataCommandHandler; +import org.xbib.files.ftp.mock.test.AbstractTestCase; + +import java.util.ListResourceBundle; +import java.util.ResourceBundle; + +/** + * Tests for AbstractStubDataCommandHandler. + * + * @author Chris Mair + */ +class AbstractStubDataCommandHandlerTest extends AbstractTestCase { + + private static final Command COMMAND = new Command("command", EMPTY); + private static final InvocationRecord INVOCATION_RECORD = new InvocationRecord(COMMAND, DEFAULT_HOST); + + private static final String REPLY_TEXT150 = "reply 150 ... abcdef"; + private static final String REPLY_TEXT226 = "reply 226 ... abcdef"; + private static final String REPLY_TEXT222 = "reply 222 ... abcdef"; + private static final String REPLY_TEXT333 = "reply 333 ... abcdef"; + private static final String REPLY_TEXT444 = "reply 444 ... abcdef"; + + private Session session; + private ResourceBundle replyTextBundle; + private AbstractStubDataCommandHandler commandHandler; + + @Test + void testHandleCommand() throws Exception { + // Define CommandHandler test subclass + commandHandler = new AbstractStubDataCommandHandler() { + protected void beforeProcessData(Command c, Session s, InvocationRecord ir) { + verifyParameters(c, s, ir); + // Send unique reply code so that we can verify proper method invocation and ordering + session.sendReply(222, REPLY_TEXT222); + } + + protected void processData(Command c, Session s, InvocationRecord ir) { + verifyParameters(c, s, ir); + // Send unique reply code so that we can verify proper method invocation and ordering + session.sendReply(333, REPLY_TEXT333); + } + + protected void afterProcessData(Command c, Session s, InvocationRecord ir) { + verifyParameters(c, s, ir); + // Send unique reply code so that we can verify proper method invocation and ordering + session.sendReply(444, REPLY_TEXT444); + } + + private void verifyParameters(Command c, Session s, InvocationRecord ir) { + assertSame(COMMAND, c); + assertSame(session, s); + assertSame(INVOCATION_RECORD, ir); + } + }; + + commandHandler.setReplyTextBundle(replyTextBundle); + commandHandler.handleCommand(COMMAND, session, INVOCATION_RECORD); + + verify(session).sendReply(150, REPLY_TEXT150); + verify(session).openDataConnection(); + verify(session).sendReply(222, REPLY_TEXT222); + verify(session).sendReply(333, REPLY_TEXT333); + verify(session).sendReply(444, REPLY_TEXT444); + verify(session).closeDataConnection(); + verify(session).sendReply(226, REPLY_TEXT226); + } + + @Test + void testHandleCommand_OverrideInitialReplyCodeAndText() throws Exception { + final int OVERRIDE_REPLY_CODE = 333; + final String OVERRIDE_REPLY_TEXT = "reply text"; + + commandHandler.setPreliminaryReplyCode(OVERRIDE_REPLY_CODE); + commandHandler.setPreliminaryReplyText(OVERRIDE_REPLY_TEXT); + commandHandler.setReplyTextBundle(replyTextBundle); + commandHandler.handleCommand(COMMAND, session, INVOCATION_RECORD); + + verify(session).sendReply(OVERRIDE_REPLY_CODE, OVERRIDE_REPLY_TEXT); + verify(session).openDataConnection(); + verify(session).closeDataConnection(); + verify(session).sendReply(226, REPLY_TEXT226); + } + + @Test + void testSetPreliminaryReplyCode_Invalid() { + assertThrows(AssertFailedException.class, () -> commandHandler.setPreliminaryReplyCode(0)); + } + + @Test + void testSetFinalReplyCode_Invalid() { + assertThrows(AssertFailedException.class, () -> commandHandler.setFinalReplyCode(0)); + } + + //------------------------------------------------------------------------- + // Test setup + //------------------------------------------------------------------------- + + @BeforeEach + void setUp() throws Exception { + session = mock(Session.class); + replyTextBundle = new ListResourceBundle() { + protected Object[][] getContents() { + return new Object[][] { + { Integer.toString(150), REPLY_TEXT150 }, + { Integer.toString(222), REPLY_TEXT222 }, + { Integer.toString(226), REPLY_TEXT226 }, + { Integer.toString(333), REPLY_TEXT333 }, + { Integer.toString(444), REPLY_TEXT444 }, + }; + } + }; + commandHandler = new AbstractStubDataCommandHandler() { + protected void processData(Command c, Session s, InvocationRecord ir) { + } + }; + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AcctCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AcctCommandHandlerTest.java new file mode 100644 index 0000000..846126b --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AcctCommandHandlerTest.java @@ -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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.AcctCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the AcctCommandHandler class + * + * @author Chris Mair + */ +class AcctCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final String ACCOUNT1 = "account1"; + private static final String ACCOUNT2 = "account2"; + + private AcctCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.ACCT_OK, replyTextFor(ReplyCodes.ACCT_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), AcctCommandHandler.ACCOUNT_KEY, ACCOUNT1); + verifyOneDataElement(commandHandler.getInvocation(1), AcctCommandHandler.ACCOUNT_KEY, ACCOUNT2); + } + + @Test + void testHandleCommand_MissingPasswordParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.ACCT, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new AcctCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.ACCT, array(ACCOUNT1)); + command2 = new Command(CommandNames.ACCT, array(ACCOUNT2)); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AlloCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AlloCommandHandlerTest.java new file mode 100644 index 0000000..7a13322 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AlloCommandHandlerTest.java @@ -0,0 +1,105 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.stub.command.AlloCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the AlloCommandHandler class + * + * @author Chris Mair + */ +class AlloCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final int BYTES1 = 64; + private static final int BYTES2 = 555; + private static final int RECORD_SIZE = 77; + + private AlloCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.ALLO_OK, replyTextFor(ReplyCodes.ALLO_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), AlloCommandHandler.NUMBER_OF_BYTES_KEY, + BYTES1); + verifyTwoDataElements(commandHandler.getInvocation(1), AlloCommandHandler.NUMBER_OF_BYTES_KEY, + BYTES2, AlloCommandHandler.RECORD_SIZE_KEY, RECORD_SIZE); + } + + @Test + void testHandleCommand_MissingNumberOfBytesParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.ALLO, EMPTY); + } + + @Test + void testHandleCommand_RecordSizeDelimiterWithoutValue() throws Exception { + try { + commandHandler.handleCommand(new Command(CommandNames.ALLO, array("123 R ")), session); + fail("Expected AssertFailedException"); + } + catch (AssertFailedException expected) { + log("Expected: " + expected); + } + } + + @Test + void testHandleCommand_InvalidNumberOfBytesParameter() throws Exception { + try { + commandHandler.handleCommand(new Command(CommandNames.ALLO, array("xx")), session); + fail("Expected NumberFormatException"); + } + catch (NumberFormatException expected) { + log("Expected: " + expected); + } + } + + @Test + void testHandleCommand_InvalidRecordSizeParameter() throws Exception { + try { + commandHandler.handleCommand(new Command(CommandNames.ALLO, array("123 R xx")), session); + fail("Expected NumberFormatException"); + } + catch (NumberFormatException expected) { + log("Expected: " + expected); + } + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new AlloCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.ALLO, array(Integer.toString(BYTES1))); + command2 = new Command(CommandNames.ALLO, array(Integer.toString(BYTES2) + " R " + Integer.toString(RECORD_SIZE))); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AppeCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AppeCommandHandlerTest.java new file mode 100644 index 0000000..f75fb80 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/AppeCommandHandlerTest.java @@ -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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.AppeCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the AppeCommandHandler class + * + * @author Chris Mair + */ +class AppeCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private AppeCommandHandler commandHandler; + + @BeforeEach + void setUp() throws Exception { + commandHandler = new AppeCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + + @Test + void testHandleCommand() throws Exception { + final String DATA = "ABC"; + + when(session.readData()).thenReturn(DATA.getBytes()); + + Command command = new Command(CommandNames.APPE, array(FILENAME1)); + commandHandler.handleCommand(command, session); + + verify(session).sendReply(ReplyCodes.TRANSFER_DATA_INITIAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_INITIAL_OK)); + verify(session).openDataConnection(); + verify(session).closeDataConnection(); + verify(session).sendReply(ReplyCodes.TRANSFER_DATA_FINAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_FINAL_OK)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyTwoDataElements(commandHandler.getInvocation(0), AppeCommandHandler.PATHNAME_KEY, FILENAME1, + AppeCommandHandler.FILE_CONTENTS_KEY, DATA.getBytes()); + } + + @Test + void testHandleCommand_MissingPathnameParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.APPE, EMPTY); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/CdupCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/CdupCommandHandlerTest.java new file mode 100644 index 0000000..f0d9710 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/CdupCommandHandlerTest.java @@ -0,0 +1,58 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.CdupCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the CdupCommandHandler class + * + * @author Chris Mair + */ +class CdupCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private CdupCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.CDUP_OK, replyTextFor(ReplyCodes.CDUP_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyNoDataElements(commandHandler.getInvocation(0)); + verifyNoDataElements(commandHandler.getInvocation(1)); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new CdupCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.CDUP, EMPTY); + command2 = new Command(CommandNames.CDUP, EMPTY); + } +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/CwdCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/CwdCommandHandlerTest.java new file mode 100644 index 0000000..02da9ff --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/CwdCommandHandlerTest.java @@ -0,0 +1,63 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.CwdCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the CwdCommandHandler class + * + * @author Chris Mair + */ +class CwdCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private CwdCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.CWD_OK, replyTextFor(ReplyCodes.CWD_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), CwdCommandHandler.PATHNAME_KEY, DIR1); + verifyOneDataElement(commandHandler.getInvocation(1), CwdCommandHandler.PATHNAME_KEY, DIR2); + } + + @Test + void testHandleCommand_MissingPathnameParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.CWD, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new CwdCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.CWD, array(DIR1)); + command2 = new Command(CommandNames.CWD, array(DIR2)); + } +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/DeleCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/DeleCommandHandlerTest.java new file mode 100644 index 0000000..0a83511 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/DeleCommandHandlerTest.java @@ -0,0 +1,62 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.DeleCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the DeleCommandHandler class + * + * @author Chris Mair + */ +class DeleCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private DeleCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + verify(session, times(2)).sendReply(ReplyCodes.DELE_OK, replyTextFor(ReplyCodes.DELE_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), DeleCommandHandler.PATHNAME_KEY, FILENAME1); + verifyOneDataElement(commandHandler.getInvocation(1), DeleCommandHandler.PATHNAME_KEY, FILENAME2); + } + + @Test + void testHandleCommand_MissingPathnameParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.DELE, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new DeleCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.DELE, array(FILENAME1)); + command2 = new Command(CommandNames.DELE, array(FILENAME2)); + } +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/EprtCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/EprtCommandHandlerTest.java new file mode 100644 index 0000000..27cb088 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/EprtCommandHandlerTest.java @@ -0,0 +1,90 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.EprtCommandHandler; +import org.xbib.files.ftp.mock.stub.command.PortCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +import java.net.InetAddress; + +/** + * Tests for the EprtCommandHandler class + * + * @author Chris Mair + */ +class EprtCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final String[] PARAMETERS_INSUFFICIENT = EMPTY; + private static final String[] PARAMETERS_IPV4 = {"|1|132.235.1.2|6275|"}; + private static final InetAddress HOST_IPV4 = inetAddress("132.235.1.2"); + private static final String[] PARAMETERS_IPV6 = {"|2|1080::8:800:200C:417A|6275|"}; + private static final InetAddress HOST_IPV6 = inetAddress("1080::8:800:200C:417A"); + private static final int PORT = 6275; + + private EprtCommandHandler commandHandler; + + @Test + void testHandleCommand_IPv4() throws Exception { + final Command COMMAND = new Command(CommandNames.EPRT, PARAMETERS_IPV4); + + commandHandler.handleCommand(COMMAND, session); + + verify(session).setClientDataPort(PORT); + verify(session).setClientDataHost(HOST_IPV4); + verify(session).sendReply(ReplyCodes.EPRT_OK, replyTextFor(ReplyCodes.EPRT_OK)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyTwoDataElements(commandHandler.getInvocation(0), + PortCommandHandler.HOST_KEY, HOST_IPV4, + PortCommandHandler.PORT_KEY, PORT); + } + + @Test + void testHandleCommand_IPv6() throws Exception { + final Command COMMAND = new Command(CommandNames.EPRT, PARAMETERS_IPV6); + + commandHandler.handleCommand(COMMAND, session); + + verify(session).setClientDataPort(PORT); + verify(session).setClientDataHost(HOST_IPV6); + verify(session).sendReply(ReplyCodes.EPRT_OK, replyTextFor(ReplyCodes.EPRT_OK)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyTwoDataElements(commandHandler.getInvocation(0), + PortCommandHandler.HOST_KEY, HOST_IPV6, + PortCommandHandler.PORT_KEY, PORT); + } + + @Test + void testHandleCommand_MissingRequiredParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.EPRT, PARAMETERS_INSUFFICIENT); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new EprtCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/EpsvCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/EpsvCommandHandlerTest.java new file mode 100644 index 0000000..61eda33 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/EpsvCommandHandlerTest.java @@ -0,0 +1,62 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.EpsvCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +import java.net.InetAddress; + +/** + * Tests for the EpsvCommandHandler class + * + * @author Chris Mair + */ +class EpsvCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final InetAddress SERVER = inetAddress("1080::8:800:200C:417A"); + private static final int PORT = 6275; + + private EpsvCommandHandler commandHandler; + + @Test + void testHandleCommand() throws Exception { + when(session.switchToPassiveMode()).thenReturn(PORT); + when(session.getServerHost()).thenReturn(SERVER); + + final Command COMMAND = new Command(CommandNames.EPSV, EMPTY); + + commandHandler.handleCommand(COMMAND, session); + verify(session).sendReply(ReplyCodes.EPSV_OK, formattedReplyTextFor(ReplyCodes.EPSV_OK, Integer.toString(PORT))); + + verifyNumberOfInvocations(commandHandler, 1); + verifyNoDataElements(commandHandler.getInvocation(0)); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new EpsvCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} \ No newline at end of file diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/FileRetrCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/FileRetrCommandHandlerTest.java new file mode 100644 index 0000000..ef8f00c --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/FileRetrCommandHandlerTest.java @@ -0,0 +1,112 @@ +/* + * 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.test.stub.command; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.stub.command.FileRetrCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +import java.util.Arrays; + +/** + * Tests for the FileRetrCommandHandler class + * + * @author Chris Mair + */ +class FileRetrCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final byte BYTE1 = (byte) 7; + private static final byte BYTE2 = (byte) 21; + + private FileRetrCommandHandler commandHandler; + + @Test + void testConstructor_String_Null() { + assertThrows(AssertFailedException.class, () -> new FileRetrCommandHandler(null)); + } + + @Test + void testSetFile_Null() { + assertThrows(AssertFailedException.class, () -> commandHandler.setFile(null)); + } + + @Test + void testHandleCommand() throws Exception { + final byte[] BUFFER = new byte[FileRetrCommandHandler.BUFFER_SIZE]; + Arrays.fill(BUFFER, BYTE1); + + session.sendReply(ReplyCodes.TRANSFER_DATA_INITIAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_INITIAL_OK)); + session.openDataConnection(); + + commandHandler.setFile("Sample.jpg"); + Command command = new Command(CommandNames.RETR, array(FILENAME1)); + commandHandler.handleCommand(command, session); + + verify(session, times(5)).sendData(any(), eq(512)); + verify(session).sendData(any(), eq(3)); + verify(session).closeDataConnection(); + verify(session).sendReply(ReplyCodes.TRANSFER_DATA_FINAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_FINAL_OK)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyOneDataElement(commandHandler.getInvocation(0), FileRetrCommandHandler.PATHNAME_KEY, FILENAME1); + } + + @Test + void testHandleCommand_MissingPathnameParameter() throws Exception { + commandHandler.setFile("abc.txt"); // this property must be set + testHandleCommand_InvalidParameters(commandHandler, CommandNames.RETR, EMPTY); + } + + @Test + void testHandleCommand_FileNotSet() throws Exception { + assertThrows(AssertFailedException.class, () -> commandHandler.handleCommand(new Command(CommandNames.RETR, EMPTY), session)); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new FileRetrCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +// /** +// * Create a sample binary file; 5 buffers full plus 3 extra bytes +// */ +// private void createSampleFile() { +// final String FILE_PATH = "test/org.mockftpserver/command/Sample.jpg"; +// final byte[] BUFFER = new byte[FileRetrCommandHandler.BUFFER_SIZE]; +// Arrays.fill(BUFFER, BYTE1); +// +// File file = new File(FILE_PATH); +// FileOutputStream out = new FileOutputStream(file); +// for (int i = 0; i < 5; i++) { +// out.write(BUFFER); +// } +// Arrays.fill(BUFFER, BYTE2); +// out.write(BUFFER, 0, 3); +// out.close(); +// LOG.info("Created temporary file [" + FILE_PATH + "]: length=" + file.length()); +// } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/HelpCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/HelpCommandHandlerTest.java new file mode 100644 index 0000000..35b9e30 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/HelpCommandHandlerTest.java @@ -0,0 +1,62 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.HelpCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the HelpCommandHandler class + * + * @author Chris Mair + */ +class HelpCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private HelpCommandHandler commandHandler; + + @Test + void testHandleCommand() throws Exception { + + final String RESPONSE_DATA = "help for ABC..."; + commandHandler.setHelpMessage(RESPONSE_DATA); + + final Command COMMAND1 = new Command(CommandNames.HELP, EMPTY); + final Command COMMAND2 = new Command(CommandNames.HELP, array("abc")); + + commandHandler.handleCommand(COMMAND1, session); + commandHandler.handleCommand(COMMAND2, session); + + verify(session, times(2)).sendReply(ReplyCodes.HELP_OK, formattedReplyTextFor(ReplyCodes.HELP_OK, RESPONSE_DATA)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), HelpCommandHandler.COMMAND_NAME_KEY, null); + verifyOneDataElement(commandHandler.getInvocation(1), HelpCommandHandler.COMMAND_NAME_KEY, "abc"); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new HelpCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/ListCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/ListCommandHandlerTest.java new file mode 100644 index 0000000..6e76143 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/ListCommandHandlerTest.java @@ -0,0 +1,66 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.ListCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the ListCommandHandler class + * + * @author Chris Mair + */ +class ListCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private ListCommandHandler commandHandler; + + @Test + void testHandleCommand() throws Exception { + final String DIR_LISTING = " directory listing\nabc.txt\ndef.log\n"; + final String DIR_LISTING_TRIMMED = DIR_LISTING.trim(); + ((ListCommandHandler) commandHandler).setDirectoryListing(DIR_LISTING); + + Command command1 = new Command(CommandNames.LIST, array(DIR1)); + Command command2 = new Command(CommandNames.LIST, EMPTY); + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.TRANSFER_DATA_INITIAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_INITIAL_OK)); + verify(session, times(2)).openDataConnection(); + byte[] bytes = DIR_LISTING_TRIMMED.getBytes(); + verify(session, times(2)).sendData(bytes, bytes.length); + verify(session, times(2)).closeDataConnection(); + verify(session, times(2)).sendReply(ReplyCodes.TRANSFER_DATA_FINAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_FINAL_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), ListCommandHandler.PATHNAME_KEY, DIR1); + verifyOneDataElement(commandHandler.getInvocation(1), ListCommandHandler.PATHNAME_KEY, null); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new ListCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/MkdCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/MkdCommandHandlerTest.java new file mode 100644 index 0000000..c91a1f6 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/MkdCommandHandlerTest.java @@ -0,0 +1,65 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.MkdCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the MkdCommandHandler class + * + * @author Chris Mair + */ +class MkdCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private MkdCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session).sendReply(ReplyCodes.MKD_OK, formattedReplyTextFor(ReplyCodes.MKD_OK, DIR1)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), MkdCommandHandler.PATHNAME_KEY, DIR1); + verifyOneDataElement(commandHandler.getInvocation(1), MkdCommandHandler.PATHNAME_KEY, DIR2); + } + + @Test + void testHandleCommand_MissingPathnameParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.MKD, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new MkdCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.MKD, array(DIR1)); + command2 = new Command(CommandNames.MKD, array(DIR2)); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/ModeCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/ModeCommandHandlerTest.java new file mode 100644 index 0000000..804225f --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/ModeCommandHandlerTest.java @@ -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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.ModeCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the ModeCommandHandler class + * + * @author Chris Mair + */ +class ModeCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final String CODE1 = "S"; + private static final String CODE2 = "B"; + + private ModeCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.MODE_OK, replyTextFor(ReplyCodes.MODE_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), ModeCommandHandler.MODE_KEY, CODE1); + verifyOneDataElement(commandHandler.getInvocation(1), ModeCommandHandler.MODE_KEY, CODE2); + } + + @Test + void testHandleCommand_MissingPathnameParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.MODE, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new ModeCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.MODE, array(CODE1)); + command2 = new Command(CommandNames.MODE, array(CODE2)); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/NlstCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/NlstCommandHandlerTest.java new file mode 100644 index 0000000..ceac092 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/NlstCommandHandlerTest.java @@ -0,0 +1,66 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.NlstCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the NlstCommandHandler class + * + * @author Chris Mair + */ +class NlstCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private NlstCommandHandler commandHandler; + + @Test + void testHandleCommand() throws Exception { + final String DIR_LISTING = " directory listing\nabc.txt\ndef.log\n"; + final String DIR_LISTING_TRIMMED = DIR_LISTING.trim(); + ((NlstCommandHandler) commandHandler).setDirectoryListing(DIR_LISTING); + + Command command1 = new Command(CommandNames.LIST, array(DIR1)); + Command command2 = new Command(CommandNames.LIST, EMPTY); + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.TRANSFER_DATA_INITIAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_INITIAL_OK)); + verify(session, times(2)).openDataConnection(); + byte[] bytes = DIR_LISTING_TRIMMED.getBytes(); + verify(session, times(2)).sendData(bytes, bytes.length); + verify(session, times(2)).closeDataConnection(); + verify(session, times(2)).sendReply(ReplyCodes.TRANSFER_DATA_FINAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_FINAL_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), NlstCommandHandler.PATHNAME_KEY, DIR1); + verifyOneDataElement(commandHandler.getInvocation(1), NlstCommandHandler.PATHNAME_KEY, null); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new NlstCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/NoopCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/NoopCommandHandlerTest.java new file mode 100644 index 0000000..0b898f4 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/NoopCommandHandlerTest.java @@ -0,0 +1,55 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.NoopCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the NoopCommandHandler class + * + * @author Chris Mair + */ +class NoopCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private NoopCommandHandler commandHandler; + + @Test + void testHandleCommand() throws Exception { + final Command COMMAND = new Command(CommandNames.NOOP, EMPTY); + + commandHandler.handleCommand(COMMAND, session); + + verify(session).sendReply(ReplyCodes.NOOP_OK, replyTextFor(ReplyCodes.NOOP_OK)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyNoDataElements(commandHandler.getInvocation(0)); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new NoopCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/PassCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/PassCommandHandlerTest.java new file mode 100644 index 0000000..55dcdf5 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/PassCommandHandlerTest.java @@ -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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.PassCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the PassCommandHandler class + * + * @author Chris Mair + */ +class PassCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final String PASSWORD1 = "password1"; + private static final String PASSWORD2 = "password2"; + + private PassCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.PASS_OK, replyTextFor(ReplyCodes.PASS_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), PassCommandHandler.PASSWORD_KEY, PASSWORD1); + verifyOneDataElement(commandHandler.getInvocation(1), PassCommandHandler.PASSWORD_KEY, PASSWORD2); + } + + @Test + void testHandleCommand_MissingPasswordParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.PASS, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new PassCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.PASS, array(PASSWORD1)); + command2 = new Command(CommandNames.PASS, array(PASSWORD2)); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/PasvCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/PasvCommandHandlerTest.java new file mode 100644 index 0000000..3f32297 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/PasvCommandHandlerTest.java @@ -0,0 +1,63 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.PasvCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +import java.net.InetAddress; + +/** + * Tests for the PasvCommandHandler class + * + * @author Chris Mair + */ +class PasvCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final int PORT = (23 << 8) + 77; + + private PasvCommandHandler commandHandler; + + @Test + void testHandleCommand() throws Exception { + final InetAddress SERVER = inetAddress("192.168.0.2"); + when(session.switchToPassiveMode()).thenReturn(PORT); + when(session.getServerHost()).thenReturn(SERVER); + + final Command COMMAND = new Command(CommandNames.PASV, EMPTY); + + commandHandler.handleCommand(COMMAND, session); + + verify(session).sendReply(ReplyCodes.PASV_OK, formattedReplyTextFor(227, "(192,168,0,2,23,77)")); + + verifyNumberOfInvocations(commandHandler, 1); + verifyNoDataElements(commandHandler.getInvocation(0)); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new PasvCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/PortCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/PortCommandHandlerTest.java new file mode 100644 index 0000000..6919068 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/PortCommandHandlerTest.java @@ -0,0 +1,71 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.PortCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +import java.net.InetAddress; + +/** + * Tests for the PortCommandHandler class + * + * @author Chris Mair + */ +class PortCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final String[] PARAMETERS = new String[]{"11", "22", "33", "44", "1", "206"}; + private static final String[] PARAMETERS_INSUFFICIENT = new String[]{"7", "29", "99", "11", "77"}; + private static final int PORT = (1 << 8) + 206; + private static final InetAddress HOST = inetAddress("11.22.33.44"); + + private PortCommandHandler commandHandler; + + @Test + void testHandleCommand() throws Exception { + final Command COMMAND = new Command(CommandNames.PORT, PARAMETERS); + + commandHandler.handleCommand(COMMAND, session); + + verify(session).setClientDataPort(PORT); + verify(session).setClientDataHost(HOST); + verify(session).sendReply(ReplyCodes.PORT_OK, replyTextFor(ReplyCodes.PORT_OK)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyTwoDataElements(commandHandler.getInvocation(0), + PortCommandHandler.HOST_KEY, HOST, + PortCommandHandler.PORT_KEY, new Integer(PORT)); + } + + @Test + void testHandleCommand_InsufficientParameters() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.PORT, PARAMETERS_INSUFFICIENT); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new PortCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/PwdCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/PwdCommandHandlerTest.java new file mode 100644 index 0000000..32c24d6 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/PwdCommandHandlerTest.java @@ -0,0 +1,59 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.PwdCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the PwdCommandHandler class + * + * @author Chris Mair + */ +class PwdCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private PwdCommandHandler commandHandler; + + @Test + void testHandleCommand() throws Exception { + + final String RESPONSE_DATA = "current dir 1"; + commandHandler.setDirectory(RESPONSE_DATA); + + final Command COMMAND = new Command(CommandNames.PWD, EMPTY); + + commandHandler.handleCommand(COMMAND, session); + + verify(session).sendReply(ReplyCodes.PWD_OK, formattedReplyTextFor(ReplyCodes.PWD_OK, "\"" + RESPONSE_DATA + "\"")); + + verifyNumberOfInvocations(commandHandler, 1); + verifyNoDataElements(commandHandler.getInvocation(0)); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new PwdCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/QuitCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/QuitCommandHandlerTest.java new file mode 100644 index 0000000..902fc28 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/QuitCommandHandlerTest.java @@ -0,0 +1,56 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.QuitCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the QuitCommandHandler class + * + * @author Chris Mair + */ +class QuitCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private QuitCommandHandler commandHandler; + + @Test + void testHandleCommand() throws Exception { + final Command COMMAND = new Command(CommandNames.QUIT, EMPTY); + + commandHandler.handleCommand(COMMAND, session); + + verify(session).sendReply(ReplyCodes.QUIT_OK, replyTextFor(ReplyCodes.QUIT_OK)); + verify(session).close(); + + verifyNumberOfInvocations(commandHandler, 1); + verifyNoDataElements(commandHandler.getInvocation(0)); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new QuitCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/ReinCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/ReinCommandHandlerTest.java new file mode 100644 index 0000000..b208186 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/ReinCommandHandlerTest.java @@ -0,0 +1,58 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.ReinCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the ReinCommandHandler class + * + * @author Chris Mair + */ +class ReinCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private ReinCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.REIN_OK, replyTextFor(ReplyCodes.REIN_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyNoDataElements(commandHandler.getInvocation(0)); + verifyNoDataElements(commandHandler.getInvocation(1)); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new ReinCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.REIN, EMPTY); + command2 = new Command(CommandNames.REIN, EMPTY); + } +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RestCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RestCommandHandlerTest.java new file mode 100644 index 0000000..864f29c --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RestCommandHandlerTest.java @@ -0,0 +1,69 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.RestCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the RestCommandHandler class + * + * @author Chris Mair + */ +class RestCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final String MARKER1 = "marker1"; + private static final String MARKER2 = "marker2"; + + private RestCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.REST_OK, replyTextFor(ReplyCodes.REST_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), RestCommandHandler.MARKER_KEY, MARKER1); + verifyOneDataElement(commandHandler.getInvocation(1), RestCommandHandler.MARKER_KEY, MARKER2); + } + + @Test + void testHandleCommand_MissingPathnameParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.REST, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new RestCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.REST, array(MARKER1)); + command2 = new Command(CommandNames.REST, array(MARKER2)); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RetrCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RetrCommandHandlerTest.java new file mode 100644 index 0000000..617f587 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RetrCommandHandlerTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2021 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.test.stub.command; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.stub.command.RetrCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the RetrCommandHandler class + * + * @author Chris Mair + */ +class RetrCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private RetrCommandHandler commandHandler; + + @Test + void testConstructor_String_Null() { + assertThrows(AssertFailedException.class, () -> new RetrCommandHandler((String) null)); + } + + @Test + void testConstructor_ByteArray_Null() { + assertThrows(AssertFailedException.class, () -> new RetrCommandHandler((byte[]) null)); + } + + @Test + void testSetFileContents_String_Null() { + assertThrows(AssertFailedException.class, () -> commandHandler.setFileContents((String) null)); + } + + @Test + void testSetFileContents_ByteArray_Null() { + assertThrows(AssertFailedException.class, () -> commandHandler.setFileContents((byte[]) null)); + } + + @Test + void testHandleCommand() throws Exception { + final String FILE_CONTENTS = "abc_123 456"; + commandHandler.setFileContents(FILE_CONTENTS); + + Command command = new Command(CommandNames.RETR, array(FILENAME1)); + commandHandler.handleCommand(command, session); + + verify(session).sendReply(ReplyCodes.TRANSFER_DATA_INITIAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_INITIAL_OK)); + verify(session).openDataConnection(); + verify(session).sendData(FILE_CONTENTS.getBytes(), FILE_CONTENTS.length()); + verify(session).closeDataConnection(); + verify(session).sendReply(ReplyCodes.TRANSFER_DATA_FINAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_FINAL_OK)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyOneDataElement(commandHandler.getInvocation(0), RetrCommandHandler.PATHNAME_KEY, FILENAME1); + } + + @Test + void testHandleCommand_MissingPathnameParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.RETR, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new RetrCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RmdCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RmdCommandHandlerTest.java new file mode 100644 index 0000000..7c8bf7c --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RmdCommandHandlerTest.java @@ -0,0 +1,64 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.RmdCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the RmdCommandHandler class + * + * @author Chris Mair + */ +class RmdCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private RmdCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.RMD_OK, replyTextFor(ReplyCodes.RMD_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), RmdCommandHandler.PATHNAME_KEY, DIR1); + verifyOneDataElement(commandHandler.getInvocation(1), RmdCommandHandler.PATHNAME_KEY, DIR2); + } + + @Test + void testHandleCommand_MissingPathnameParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.RMD, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new RmdCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.RMD, array(DIR1)); + command2 = new Command(CommandNames.RMD, array(DIR2)); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RnfrCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RnfrCommandHandlerTest.java new file mode 100644 index 0000000..3ddbaaf --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RnfrCommandHandlerTest.java @@ -0,0 +1,64 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.RnfrCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the RnfrCommandHandler class + * + * @author Chris Mair + */ +class RnfrCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private RnfrCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.RNFR_OK, replyTextFor(ReplyCodes.RNFR_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), RnfrCommandHandler.PATHNAME_KEY, FILENAME1); + verifyOneDataElement(commandHandler.getInvocation(1), RnfrCommandHandler.PATHNAME_KEY, FILENAME2); + } + + @Test + void testHandleCommand_MissingPathnameParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.RNFR, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new RnfrCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.RNFR, array(FILENAME1)); + command2 = new Command(CommandNames.RNFR, array(FILENAME2)); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RntoCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RntoCommandHandlerTest.java new file mode 100644 index 0000000..6b0d616 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/RntoCommandHandlerTest.java @@ -0,0 +1,64 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.RntoCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the RntoCommandHandler class + * + * @author Chris Mair + */ +class RntoCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private RntoCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.RNTO_OK, replyTextFor(ReplyCodes.RNTO_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), RntoCommandHandler.PATHNAME_KEY, FILENAME1); + verifyOneDataElement(commandHandler.getInvocation(1), RntoCommandHandler.PATHNAME_KEY, FILENAME2); + } + + @Test + void testHandleCommand_MissingPathnameParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.RNTO, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new RntoCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.RNTO, array(FILENAME1)); + command2 = new Command(CommandNames.RNTO, array(FILENAME2)); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/SiteCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/SiteCommandHandlerTest.java new file mode 100644 index 0000000..e7df71b --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/SiteCommandHandlerTest.java @@ -0,0 +1,66 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.SiteCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the SiteCommandHandler class + * + * @author Chris Mair + */ +class SiteCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final String PARAMETERS1 = "abc def"; + private static final String PARAMETERS2 = "abc,23,def"; + + private SiteCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.SITE_OK, replyTextFor(ReplyCodes.SITE_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), SiteCommandHandler.PARAMETERS_KEY, PARAMETERS1); + verifyOneDataElement(commandHandler.getInvocation(1), SiteCommandHandler.PARAMETERS_KEY, PARAMETERS2); + } + + @Test + void testHandleCommand_MissingParameters() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.SITE, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new SiteCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.SITE, array(PARAMETERS1)); + command2 = new Command(CommandNames.SITE, array(PARAMETERS2)); + } +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/SmntCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/SmntCommandHandlerTest.java new file mode 100644 index 0000000..d35e433 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/SmntCommandHandlerTest.java @@ -0,0 +1,63 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.SmntCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the SmntCommandHandler class + * + * @author Chris Mair + */ +class SmntCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private SmntCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.SMNT_OK, replyTextFor(ReplyCodes.SMNT_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), SmntCommandHandler.PATHNAME_KEY, DIR1); + verifyOneDataElement(commandHandler.getInvocation(1), SmntCommandHandler.PATHNAME_KEY, DIR2); + } + + @Test + void testHandleCommand_MissingPathnameParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.SMNT, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new SmntCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.SMNT, array(DIR1)); + command2 = new Command(CommandNames.SMNT, array(DIR2)); + } +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/StatCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/StatCommandHandlerTest.java new file mode 100644 index 0000000..05524ba --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/StatCommandHandlerTest.java @@ -0,0 +1,84 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.StatCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the StatCommandHandler class + * + * @author Chris Mair + */ +class StatCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final String RESPONSE_DATA = "status info 123.456"; + private static final String PATHNAME = "dir/file"; + + private StatCommandHandler commandHandler; + + @Test + void testHandleCommand_NoPathname() throws Exception { + final Command COMMAND = new Command(CommandNames.STAT, EMPTY); + commandHandler.setStatus(RESPONSE_DATA); + commandHandler.handleCommand(COMMAND, session); + + verify(session).sendReply(ReplyCodes.STAT_SYSTEM_OK, formattedReplyTextFor(ReplyCodes.STAT_SYSTEM_OK, RESPONSE_DATA)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyOneDataElement(commandHandler.getInvocation(0), StatCommandHandler.PATHNAME_KEY, null); + } + + @Test + void testHandleCommand_Pathname() throws Exception { + final Command COMMAND = new Command(CommandNames.STAT, array(PATHNAME)); + + commandHandler.setStatus(RESPONSE_DATA); + commandHandler.handleCommand(COMMAND, session); + + verify(session).sendReply(ReplyCodes.STAT_FILE_OK, formattedReplyTextFor(ReplyCodes.STAT_FILE_OK, RESPONSE_DATA)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyOneDataElement(commandHandler.getInvocation(0), StatCommandHandler.PATHNAME_KEY, PATHNAME); + } + + @Test + void testHandleCommand_OverrideReplyCode() throws Exception { + final Command COMMAND = new Command(CommandNames.STAT, EMPTY); + commandHandler.setStatus(RESPONSE_DATA); + commandHandler.setReplyCode(200); + commandHandler.handleCommand(COMMAND, session); + + verify(session).sendReply(200, replyTextFor(200)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyOneDataElement(commandHandler.getInvocation(0), StatCommandHandler.PATHNAME_KEY, null); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new StatCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/StorCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/StorCommandHandlerTest.java new file mode 100644 index 0000000..a99a0d3 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/StorCommandHandlerTest.java @@ -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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.StorCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the StorCommandHandler class + * + * @author Chris Mair + */ +class StorCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private StorCommandHandler commandHandler; + + @BeforeEach + void setUp() throws Exception { + commandHandler = new StorCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + + @Test + void testHandleCommand() throws Exception { + final String DATA = "ABC"; + + when(session.readData()).thenReturn(DATA.getBytes()); + + Command command = new Command(CommandNames.STOR, array(FILENAME1)); + commandHandler.handleCommand(command, session); + + verify(session).sendReply(ReplyCodes.TRANSFER_DATA_INITIAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_INITIAL_OK)); + verify(session).openDataConnection(); + verify(session).closeDataConnection(); + verify(session).sendReply(ReplyCodes.TRANSFER_DATA_FINAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_FINAL_OK)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyTwoDataElements(commandHandler.getInvocation(0), StorCommandHandler.PATHNAME_KEY, FILENAME1, + StorCommandHandler.FILE_CONTENTS_KEY, DATA.getBytes()); + } + + @Test + void testHandleCommand_MissingPathnameParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.STOR, EMPTY); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/StouCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/StouCommandHandlerTest.java new file mode 100644 index 0000000..5398831 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/StouCommandHandlerTest.java @@ -0,0 +1,63 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.StouCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the StouCommandHandler class + * + * @author Chris Mair + */ +class StouCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private StouCommandHandler commandHandler; + + @BeforeEach + void setUp() throws Exception { + commandHandler = new StouCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + + @Test + void testHandleCommand() throws Exception { + final String DATA = "ABC"; + final String FILENAME = "abc.txt"; + + when(session.readData()).thenReturn(DATA.getBytes()); + + Command command = new Command(CommandNames.STOU, array(FILENAME1)); + commandHandler.setFilename(FILENAME); + commandHandler.handleCommand(command, session); + + verify(session).sendReply(ReplyCodes.TRANSFER_DATA_INITIAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_INITIAL_OK)); + verify(session).openDataConnection(); + verify(session).closeDataConnection(); + verify(session).sendReply(ReplyCodes.TRANSFER_DATA_FINAL_OK, formattedReplyTextFor("226.WithFilename", FILENAME)); + + verifyNumberOfInvocations(commandHandler, 1); + verifyOneDataElement(commandHandler.getInvocation(0), StouCommandHandler.FILE_CONTENTS_KEY, DATA.getBytes()); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/StruCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/StruCommandHandlerTest.java new file mode 100644 index 0000000..a3270ec --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/StruCommandHandlerTest.java @@ -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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.StruCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the StruCommandHandler class + * + * @author Chris Mair + */ +class StruCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final String CODE1 = "F"; + private static final String CODE2 = "R"; + + private StruCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.handleCommand(command2, session); + + verify(session, times(2)).sendReply(ReplyCodes.STRU_OK, replyTextFor(ReplyCodes.STRU_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), StruCommandHandler.FILE_STRUCTURE_KEY, CODE1); + verifyOneDataElement(commandHandler.getInvocation(1), StruCommandHandler.FILE_STRUCTURE_KEY, CODE2); + } + + @Test + void testHandleCommand_MissingPathnameParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.STRU, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new StruCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + command1 = new Command(CommandNames.STRU, array(CODE1)); + command2 = new Command(CommandNames.STRU, array(CODE2)); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/SystCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/SystCommandHandlerTest.java new file mode 100644 index 0000000..0d901c3 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/SystCommandHandlerTest.java @@ -0,0 +1,65 @@ +/* + * 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.test.stub.command; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.core.util.AssertFailedException; +import org.xbib.files.ftp.mock.stub.command.SystCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the SystCommandHandler class + * + * @author Chris Mair + */ +class SystCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private SystCommandHandler commandHandler; + + @Test + void testHandleCommand() throws Exception { + final String SYSTEM_NAME = "UNIX"; + commandHandler.setSystemName(SYSTEM_NAME); + + final Command COMMAND = new Command(CommandNames.SYST, EMPTY); + + commandHandler.handleCommand(COMMAND, session); + + verify(session).sendReply(ReplyCodes.SYST_OK, formattedReplyTextFor(ReplyCodes.SYST_OK, "\"" + SYSTEM_NAME + "\"")); + + verifyNumberOfInvocations(commandHandler, 1); + verifyNoDataElements(commandHandler.getInvocation(0)); + } + + @Test + void testSetSystemName_Null() { + assertThrows(AssertFailedException.class, () -> commandHandler.setSystemName(null)); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new SystCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/TypeCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/TypeCommandHandlerTest.java new file mode 100644 index 0000000..93599ac --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/TypeCommandHandlerTest.java @@ -0,0 +1,66 @@ +/* + * 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.test.stub.command; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.TypeCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the TypeCommandHandler class + * + * @author Chris Mair + */ +class TypeCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private TypeCommandHandler commandHandler; + + @Test + void testHandleCommand() throws Exception { + final Command COMMAND1 = new Command("TYPE", array("A")); + final Command COMMAND2 = new Command("TYPE", array("B")); + final Command COMMAND3 = new Command("TYPE", array("L", "8")); + + commandHandler.handleCommand(COMMAND1, session); + commandHandler.handleCommand(COMMAND2, session); + commandHandler.handleCommand(COMMAND3, session); + + verify(session, times(3)).sendReply(ReplyCodes.TYPE_OK, replyTextFor(ReplyCodes.TYPE_OK)); + + verifyNumberOfInvocations(commandHandler, 3); + verifyOneDataElement(commandHandler.getInvocation(0), TypeCommandHandler.TYPE_INFO_KEY, new String[] {"A", null}); + verifyOneDataElement(commandHandler.getInvocation(1), TypeCommandHandler.TYPE_INFO_KEY, new String[] {"B", null}); + verifyOneDataElement(commandHandler.getInvocation(2), TypeCommandHandler.TYPE_INFO_KEY, new String[] {"L", "8"}); + } + + @Test + void testHandleCommand_MissingTypeParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.TYPE, EMPTY); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new TypeCommandHandler(); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/UserCommandHandlerTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/UserCommandHandlerTest.java new file mode 100644 index 0000000..41a5af8 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/command/UserCommandHandlerTest.java @@ -0,0 +1,78 @@ +/* + * 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.test.stub.command; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.Command; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.ReplyCodes; +import org.xbib.files.ftp.mock.stub.command.UserCommandHandler; +import org.xbib.files.ftp.mock.test.core.command.AbstractCommandHandlerTestCase; + +/** + * Tests for the UserCommandHandler class + * + * @author Chris Mair + */ +class UserCommandHandlerTest extends AbstractCommandHandlerTestCase { + + private static final String USERNAME1 = "user1"; + private static final String USERNAME2 = "user2"; + + private UserCommandHandler commandHandler; + private Command command1; + private Command command2; + + @Test + void testHandleCommand() throws Exception { + commandHandler.handleCommand(command1, session); + commandHandler.setPasswordRequired(false); + commandHandler.handleCommand(command2, session); + + verify(session).sendReply(ReplyCodes.USER_NEED_PASSWORD_OK, replyTextFor(ReplyCodes.USER_NEED_PASSWORD_OK)); + verify(session).sendReply(ReplyCodes.USER_LOGGED_IN_OK, replyTextFor(ReplyCodes.USER_LOGGED_IN_OK)); + + verifyNumberOfInvocations(commandHandler, 2); + verifyOneDataElement(commandHandler.getInvocation(0), UserCommandHandler.USERNAME_KEY, USERNAME1); + verifyOneDataElement(commandHandler.getInvocation(1), UserCommandHandler.USERNAME_KEY, USERNAME2); + } + + @Test + void testHandleCommand_MissingUsernameParameter() throws Exception { + testHandleCommand_InvalidParameters(commandHandler, CommandNames.USER, EMPTY); + } + + @Test + void testSetPasswordRequired() { + assertTrue(commandHandler.isPasswordRequired()); + commandHandler.setPasswordRequired(false); + assertFalse(commandHandler.isPasswordRequired()); + } + + @BeforeEach + void setUp() throws Exception { + commandHandler = new UserCommandHandler(); + command1 = new Command(CommandNames.USER, array(USERNAME1)); + command2 = new Command(CommandNames.USER, array(USERNAME2)); + commandHandler.setReplyTextBundle(replyTextBundle); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/example/FtpWorkingDirectory.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/example/FtpWorkingDirectory.java new file mode 100644 index 0000000..64f2e40 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/example/FtpWorkingDirectory.java @@ -0,0 +1,60 @@ +/* + * 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.test.stub.example; + +import java.io.IOException; +import java.net.SocketException; +import org.xbib.io.ftp.client.FTPClient; + +/** + * Simple FTP client code example. + * + * @author Chris Mair + */ +public class FtpWorkingDirectory { + + private String server; + private int port; + + /** + * Return the current working directory for the FTP account on the server + * @return the current working directory + * @throws SocketException - if a socket error occurs + * @throws IOException - if an error occurs + */ + public String getWorkingDirectory() throws SocketException, IOException { + FTPClient ftpClient = new FTPClient(); + ftpClient.connect(server, port); + return ftpClient.printWorkingDirectory(); + } + + /** + * Set the hostname of the FTP server + * @param server - the hostname of the FTP server + */ + public void setServer(String server) { + this.server = server; + } + + /** + * Set the port number for the FTP server + * @param port - the port number + */ + public void setPort(int port) { + this.port = port; + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/example/FtpWorkingDirectoryTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/example/FtpWorkingDirectoryTest.java new file mode 100644 index 0000000..b0e1803 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/example/FtpWorkingDirectoryTest.java @@ -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.test.stub.example; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.stub.StubFtpServer; +import org.xbib.files.ftp.mock.stub.command.PwdCommandHandler; +import org.xbib.files.ftp.mock.test.AbstractTestCase; +import org.xbib.files.ftp.mock.test.IntegrationTest; + +/** + * Example test using StubFtpServer, with programmatic configuration. + */ +class FtpWorkingDirectoryTest extends AbstractTestCase implements IntegrationTest { + + private static final int PORT = 9981; + private FtpWorkingDirectory ftpWorkingDirectory; + private StubFtpServer stubFtpServer; + + @Test + void testGetWorkingDirectory() throws Exception { + + // Replace the existing (default) CommandHandler; customize returned directory pathname + final String DIR = "some/dir"; + PwdCommandHandler pwdCommandHandler = new PwdCommandHandler(); + pwdCommandHandler.setDirectory(DIR); + stubFtpServer.setCommandHandler(CommandNames.PWD, pwdCommandHandler); + + stubFtpServer.start(); + + String workingDir = ftpWorkingDirectory.getWorkingDirectory(); + + assertEquals(DIR, workingDir); + } + + @BeforeEach + void setUp() throws Exception { + ftpWorkingDirectory = new FtpWorkingDirectory(); + ftpWorkingDirectory.setPort(PORT); + stubFtpServer = new StubFtpServer(); + stubFtpServer.setServerControlPort(PORT); + } + + @AfterEach + void tearDown() throws Exception { + stubFtpServer.stop(); + } + +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/example/RemoteFile.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/example/RemoteFile.java new file mode 100644 index 0000000..ebdb73d --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/example/RemoteFile.java @@ -0,0 +1,70 @@ +/* + * 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.test.stub.example; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.xbib.io.ftp.client.FTPClient; + +/** + * Simple FTP client code example. + * + * @author Chris Mair + */ +public class RemoteFile { + + public static final String USERNAME = "user"; + public static final String PASSWORD = "password"; + + private String server; + private int port; + + public String readFile(String filename) throws IOException { + + FTPClient ftpClient = new FTPClient(); + ftpClient.connect(server, port); + ftpClient.login(USERNAME, PASSWORD); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + boolean success = ftpClient.retrieveFile(filename, outputStream); + ftpClient.disconnect(); + + if (!success) { + throw new IOException("Retrieve file failed: " + filename); + } + return outputStream.toString(); + } + + /** + * Set the hostname of the FTP server + * + * @param server - the hostname of the FTP server + */ + public void setServer(String server) { + this.server = server; + } + + /** + * Set the port number for the FTP server + * + * @param port - the port number + */ + public void setPort(int port) { + this.port = port; + } + + // Other methods ... +} diff --git a/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/example/RemoteFileTest.java b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/example/RemoteFileTest.java new file mode 100644 index 0000000..5d32297 --- /dev/null +++ b/files-ftp-mock/src/test/java/org/xbib/files/ftp/mock/test/stub/example/RemoteFileTest.java @@ -0,0 +1,99 @@ +/* + * 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.test.stub.example; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.files.ftp.mock.core.command.CommandNames; +import org.xbib.files.ftp.mock.core.command.InvocationRecord; +import org.xbib.files.ftp.mock.stub.StubFtpServer; +import org.xbib.files.ftp.mock.stub.command.RetrCommandHandler; +import org.xbib.files.ftp.mock.test.AbstractTestCase; +import org.xbib.files.ftp.mock.test.IntegrationTest; + +import java.io.IOException; + +/** + * Example test using StubFtpServer, with programmatic configuration. + */ +class RemoteFileTest extends AbstractTestCase implements IntegrationTest { + + private static final int PORT = 9981; + private static final String FILENAME = "dir/sample.txt"; + + private RemoteFile remoteFile; + private StubFtpServer stubFtpServer; + + @Test + void testReadFile() throws Exception { + + final String CONTENTS = "abcdef 1234567890"; + + // Replace the default RETR CommandHandler; customize returned file contents + RetrCommandHandler retrCommandHandler = new RetrCommandHandler(); + retrCommandHandler.setFileContents(CONTENTS); + stubFtpServer.setCommandHandler(CommandNames.RETR, retrCommandHandler); + + stubFtpServer.start(); + + String contents = remoteFile.readFile(FILENAME); + + // Verify returned file contents + assertEquals(CONTENTS, contents); + + // Verify the submitted filename + InvocationRecord invocationRecord = retrCommandHandler.getInvocation(0); + String filename = invocationRecord.getString(RetrCommandHandler.PATHNAME_KEY); + assertEquals(FILENAME, filename); + } + + @Test + void testReadFileThrowsException() { + + // Replace the default RETR CommandHandler; return failure reply code + RetrCommandHandler retrCommandHandler = new RetrCommandHandler(); + retrCommandHandler.setFinalReplyCode(550); + stubFtpServer.setCommandHandler(CommandNames.RETR, retrCommandHandler); + + stubFtpServer.start(); + + try { + remoteFile.readFile(FILENAME); + fail("Expected IOException"); + } + catch (IOException expected) { + // Expected this + } + } + + @BeforeEach + void setUp() throws Exception { + remoteFile = new RemoteFile(); + remoteFile.setServer("localhost"); + remoteFile.setPort(PORT); + stubFtpServer = new StubFtpServer(); + stubFtpServer.setServerControlPort(PORT); + } + + @AfterEach + void tearDown() throws Exception { + stubFtpServer.stop(); + } + +} diff --git a/files-ftp/src/main/java/org/xbib/io/ftp/client/parser/FTPTimestampParserImpl.java b/files-ftp/src/main/java/org/xbib/io/ftp/client/parser/FTPTimestampParserImpl.java index 4c74b07..d846937 100644 --- a/files-ftp/src/main/java/org/xbib/io/ftp/client/parser/FTPTimestampParserImpl.java +++ b/files-ftp/src/main/java/org/xbib/io/ftp/client/parser/FTPTimestampParserImpl.java @@ -76,7 +76,7 @@ public class FTPTimestampParserImpl implements Configurable { * unit found in the date format. * Default is 0 (to avoid dropping precision) */ - private static int getEntry(SimpleDateFormat dateFormat) { + public static int getEntry(SimpleDateFormat dateFormat) { if (dateFormat == null) { return 0; } diff --git a/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFileService.java b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFileService.java index da93d8d..3854a12 100644 --- a/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFileService.java +++ b/files-sftp-fs/src/main/java/org/apache/sshd/fs/spi/SFTPFileService.java @@ -1,9 +1,5 @@ package org.apache.sshd.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 SFTPFileService 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 DEFAULT_DIR_PERMISSIONS = PosixFilePermissions.fromString("rwxr-xr-x"); @@ -133,73 +132,155 @@ public class SFTPFileService 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)); - } - - @Override - public UserPrincipal getOwner(String path) throws IOException { - return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path))); - } - - @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, String target, CopyOption... copyOptions) throws IOException { - upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, 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, String target, CopyOption... copyOptions) throws IOException { - upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); - } - - @Override - public void download(Path source, Path target, CopyOption... copyOptions) throws IOException { + public void setPosixFileAttributes(String path, + String owner, + String group, + Instant lastModifiedTime, + Instant lastAccessTime, + Instant createTime) throws IOException { performWithContext(ctx -> { - download(ctx, source, target, READ_BUFFER_SIZE, copyOptions); + 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 void download(String source, Path target, CopyOption... copyOptions) throws IOException { + 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 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 -> { - download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE, copyOptions); + PosixFileAttributeView view = Files.getFileAttributeView(ctx.fileSystem.getPath(path), + PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); + view.setGroup(ctx.fileSystem.getUserPrincipalLookupService().lookupPrincipalByGroupName(group)); return null; }); } @Override - public void download(Path source, OutputStream target) throws IOException { + 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, + 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 dirPermissions, + Set filePermissions, + CopyOption... copyOptions) throws IOException { performWithContext(ctx -> { - download(ctx, source, target, READ_BUFFER_SIZE); + upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), + dirPermissions, filePermissions, copyOptions); return null; }); } @Override - public void download(String source, OutputStream target) 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(Path source, + Path target, + Set dirPermissions, + Set filePermissions, + CopyOption... copyOptions) throws IOException { performWithContext(ctx -> { - download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE); + upload(ctx, Files.newByteChannel(source), target, dirPermissions, filePermissions, 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, + Path target, + CopyOption... copyOptions) throws IOException { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + + @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 -> { + download(ctx, ctx.fileSystem.getPath(source), target); return null; }); } @@ -249,45 +330,27 @@ public class SFTPFileService implements FileService { return FileWalker.walk(ctx, ctx.fileSystem.getPath(path), maxdepth, options); } - public void upload(Path source, Path target, + @Override + public void upload(InputStream source, + Path target, Set dirPermissions, Set filePermissions, CopyOption... copyOptions) throws IOException { performWithContext(ctx -> { - upload(ctx, Files.newByteChannel(source), target, WRITE_BUFFER_SIZE, + upload(ctx, Channels.newChannel(source), target, dirPermissions, filePermissions, copyOptions); return null; }); } - public void upload(Path source, String target, + @Override + public void upload(InputStream source, + String target, Set dirPermissions, Set filePermissions, CopyOption... copyOptions) throws IOException { performWithContext(ctx -> { - upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE, - dirPermissions, filePermissions, copyOptions); - return null; - }); - } - - public void upload(InputStream source, Path target, - Set dirPermissions, - Set filePermissions, - CopyOption... copyOptions) throws IOException { - performWithContext(ctx -> { - upload(ctx, Channels.newChannel(source), target, WRITE_BUFFER_SIZE, - dirPermissions, filePermissions, copyOptions); - return null; - }); - } - - public void upload(InputStream source, String target, - Set dirPermissions, - Set filePermissions, - CopyOption... copyOptions) throws IOException { - performWithContext(ctx -> { - upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE, + upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), dirPermissions, filePermissions, copyOptions); return null; }); @@ -296,37 +359,32 @@ public class SFTPFileService implements FileService { private void upload(SFTPContext ctx, ReadableByteChannel source, Path target, - int bufferSize, Set dirPerms, Set 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(SFTPContext 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(SFTPContext 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(SFTPContext ctx, Path source, Path target, - int bufferSize, CopyOption... copyOptions) throws IOException { prepareForRead(target); transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)), - Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize); + Files.newByteChannel(target, prepareWriteOptions(copyOptions))); } private void prepareForRead(Path path) throws IOException { @@ -387,9 +445,8 @@ public class SFTPFileService 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(); diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/client/BindInfo.java b/files-webdav/src/main/java/org/xbib/io/webdav/client/BindInfo.java index 5b0c8c4..fc2f863 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/client/BindInfo.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/client/BindInfo.java @@ -37,7 +37,7 @@ public class BindInfo implements XMLizable { */ public static BindInfo createFromXml(Element root) throws DavException { if (!DomUtil.matches(root, "bind", DavConstants.NAMESPACE)) { - //log.warn("DAV:bind element expected"); + //LOG.log(Level.WARNING, "DAV:bind element expected"); throw new DavException(400); } String href = null; @@ -49,27 +49,27 @@ public class BindInfo implements XMLizable { if (segment == null) { segment = DomUtil.getText(elt); } else { - //log.warn("unexpected multiple occurrence of DAV:segment element"); + //LOG.log(Level.WARNING, "unexpected multiple occurrence of DAV:segment element"); throw new DavException(400); } } else if (DomUtil.matches(elt, "href", DavConstants.NAMESPACE)) { if (href == null) { href = DomUtil.getText(elt); } else { - //log.warn("unexpected multiple occurrence of DAV:href element"); + //LOG.log(Level.WARNING, "unexpected multiple occurrence of DAV:href element"); throw new DavException(400); } } else { - //log.warn("unexpected element " + elt.getLocalName()); + //LOG.log(Level.WARNING, "unexpected element " + elt.getLocalName()); throw new DavException(400); } } if (href == null) { - //log.warn("DAV:href element expected"); + //LOG.log(Level.WARNING, "DAV:href element expected"); throw new DavException(400); } if (segment == null) { - //log.warn("DAV:segment element expected"); + //LOG.log(Level.WARNING, "DAV:segment element expected"); throw new DavException(400); } return new BindInfo(href, segment); diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/client/LabelInfo.java b/files-webdav/src/main/java/org/xbib/io/webdav/client/LabelInfo.java index 6686f8c..2c4f67c 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/client/LabelInfo.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/client/LabelInfo.java @@ -98,7 +98,7 @@ public class LabelInfo implements XMLizable { */ public LabelInfo(Element labelElement, int depth) throws DavException { if (!DomUtil.matches(labelElement, XML_LABEL, NAMESPACE)) { - //log.warn("DAV:label element expected"); + //LOG.log(Level.WARNING, "DAV:label element expected"); throw new DavException(400); } @@ -112,7 +112,7 @@ public class LabelInfo implements XMLizable { } } if (label == null) { - //log.warn("DAV:label element must contain at least one set, add or remove element defining a label-name."); + //LOG.log(Level.WARNING, "DAV:label element must contain at least one set, add or remove element defining a label-name."); throw new DavException(400); } this.labelName = label; diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/client/MergeInfo.java b/files-webdav/src/main/java/org/xbib/io/webdav/client/MergeInfo.java index 967023f..1596ede 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/client/MergeInfo.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/client/MergeInfo.java @@ -47,7 +47,7 @@ public class MergeInfo implements XMLizable { */ public MergeInfo(Element mergeElement) throws DavException { if (!DomUtil.matches(mergeElement, XML_MERGE, NAMESPACE)) { - //log.warn("'DAV:merge' element expected"); + //LOG.log(Level.WARNING, "'DAV:merge' element expected"); throw new DavException(400); } diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/client/OptionsInfo.java b/files-webdav/src/main/java/org/xbib/io/webdav/client/OptionsInfo.java index b3345b0..c5b4b98 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/client/OptionsInfo.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/client/OptionsInfo.java @@ -90,7 +90,7 @@ public class OptionsInfo implements XMLizable { */ public static OptionsInfo createFromXml(Element optionsElement) throws DavException { if (!DomUtil.matches(optionsElement, XML_OPTIONS, NAMESPACE)) { - //log.warn("DAV:options element expected"); + //LOG.log(Level.WARNING, "DAV:options element expected"); throw new DavException(400); } OptionsInfo oInfo = new OptionsInfo(); diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/client/RebindInfo.java b/files-webdav/src/main/java/org/xbib/io/webdav/client/RebindInfo.java index ed41f82..0150ce2 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/client/RebindInfo.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/client/RebindInfo.java @@ -36,7 +36,7 @@ public class RebindInfo implements XMLizable { */ public static RebindInfo createFromXml(Element root) throws DavException { if (!DomUtil.matches(root, "rebind", DavConstants.NAMESPACE)) { - //log.warn("DAV:rebind element expected"); + //LOG.log(Level.WARNING, "DAV:rebind element expected"); throw new DavException(400); } String href = null; @@ -48,27 +48,27 @@ public class RebindInfo implements XMLizable { if (segment == null) { segment = DomUtil.getText(elt); } else { - //log.warn("unexpected multiple occurrence of DAV:segment element"); + //LOG.log(Level.WARNING, "unexpected multiple occurrence of DAV:segment element"); throw new DavException(400); } } else if (DomUtil.matches(elt, "href", DavConstants.NAMESPACE)) { if (href == null) { href = DomUtil.getText(elt); } else { - //log.warn("unexpected multiple occurrence of DAV:href element"); + //LOG.log(Level.WARNING, "unexpected multiple occurrence of DAV:href element"); throw new DavException(400); } } else { - //log.warn("unexpected element " + elt.getLocalName()); + //LOG.log(Level.WARNING, "unexpected element " + elt.getLocalName()); throw new DavException(400); } } if (href == null) { - //log.warn("DAV:href element expected"); + //LOG.log(Level.WARNING, "DAV:href element expected"); throw new DavException(400); } if (segment == null) { - //log.warn("DAV:segment element expected"); + //LOG.log(Level.WARNING, "DAV:segment element expected"); throw new DavException(400); } return new RebindInfo(href, segment); diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/client/ReportInfo.java b/files-webdav/src/main/java/org/xbib/io/webdav/client/ReportInfo.java index 2075a9c..3cb0fc6 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/client/ReportInfo.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/client/ReportInfo.java @@ -69,7 +69,7 @@ public class ReportInfo implements XMLizable { */ public ReportInfo(Element reportElement, int depth) throws DavException { if (reportElement == null) { - //log.warn("Report request body must not be null."); + //LOG.log(Level.WARNING, "Report request body must not be null."); throw new DavException(400 /*DavServletResponse.SC_BAD_REQUEST*/); } diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/client/SearchInfo.java b/files-webdav/src/main/java/org/xbib/io/webdav/client/SearchInfo.java index e1b4d0f..89bf05a 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/client/SearchInfo.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/client/SearchInfo.java @@ -202,7 +202,7 @@ public class SearchInfo implements SearchConstants, XMLizable { */ public static SearchInfo createFromXml(Element searchRequest) throws DavException { if (searchRequest == null || !XML_SEARCHREQUEST.equals(searchRequest.getLocalName())) { - //log.warn("The root element must be 'searchrequest'."); + //LOG.log(Level.WARNING, "The root element must be 'searchrequest'."); throw new DavException(400); } Element first = DomUtil.getFirstChildElement(searchRequest); @@ -218,7 +218,7 @@ public class SearchInfo implements SearchConstants, XMLizable { if (first != null) { sInfo = new SearchInfo(first.getLocalName(), DomUtil.getNamespace(first), DomUtil.getText(first), namespaces); } else { - //log.warn("A single child element is expected with the 'DAV:searchrequest'."); + //LOG.log(Level.WARNING, "A single child element is expected with the 'DAV:searchrequest'."); throw new DavException(400); } diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/client/UnbindInfo.java b/files-webdav/src/main/java/org/xbib/io/webdav/client/UnbindInfo.java index 78635ce..02b1886 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/client/UnbindInfo.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/client/UnbindInfo.java @@ -33,7 +33,7 @@ public class UnbindInfo implements XMLizable { */ public static UnbindInfo createFromXml(Element root) throws DavException { if (!DomUtil.matches(root, "unbind", DavConstants.NAMESPACE)) { - //log.warn("DAV:unbind element expected"); + //LOG.log(Level.WARNING, "DAV:unbind element expected"); throw new DavException(400); } String segment = null; @@ -44,16 +44,16 @@ public class UnbindInfo implements XMLizable { if (segment == null) { segment = DomUtil.getText(elt); } else { - //log.warn("unexpected multiple occurrence of DAV:segment element"); + //LOG.log(Level.WARNING, "unexpected multiple occurrence of DAV:segment element"); throw new DavException(400); } } else { - //log.warn("unexpected element " + elt.getLocalName()); + //LOG.log(Level.WARNING, "unexpected element " + elt.getLocalName()); throw new DavException(400); } } if (segment == null) { - //log.warn("DAV:segment element expected"); + //LOG.log(Level.WARNING, "DAV:segment element expected"); throw new DavException(400); } return new UnbindInfo(segment); diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/client/UpdateInfo.java b/files-webdav/src/main/java/org/xbib/io/webdav/client/UpdateInfo.java index 8940c8d..0f0f169 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/client/UpdateInfo.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/client/UpdateInfo.java @@ -91,7 +91,7 @@ public class UpdateInfo implements XMLizable { */ public UpdateInfo(Element updateElement) throws DavException { if (!DomUtil.matches(updateElement, XML_UPDATE, NAMESPACE)) { - //log.warn("DAV:update element expected"); + //LOG.log(Level.WARNING, "DAV:update element expected"); throw new DavException(400); } @@ -122,7 +122,7 @@ public class UpdateInfo implements XMLizable { source = new String[]{DomUtil.getChildTextTrim(wspElem, DavConstants.XML_HREF, NAMESPACE)}; type = UPDATE_BY_WORKSPACE; } else { - //log.warn("DAV:update element must contain either DAV:version, DAV:label-name or DAV:workspace child element."); + //LOG.log(Level.WARNING, "DAV:update element must contain either DAV:version, DAV:label-name or DAV:workspace child element."); throw new DavException(400); } } diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/common/DavDocumentBuilderFactory.java b/files-webdav/src/main/java/org/xbib/io/webdav/common/DavDocumentBuilderFactory.java index c7ffe24..64c944d 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/common/DavDocumentBuilderFactory.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/common/DavDocumentBuilderFactory.java @@ -37,7 +37,7 @@ public class DavDocumentBuilderFactory { * @param documentBuilderFactory */ public void setFactory(DocumentBuilderFactory documentBuilderFactory) { - //LOG.debug("DocumentBuilderFactory changed to: " + documentBuilderFactory); + //LOG.log(Level.FINE, "DocumentBuilderFactory changed to: " + documentBuilderFactory); BUILDER_FACTORY = documentBuilderFactory != null ? documentBuilderFactory : DEFAULT_FACTORY; } @@ -50,7 +50,7 @@ public class DavDocumentBuilderFactory { try { factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); } catch (ParserConfigurationException | AbstractMethodError e) { - //LOG.warn("Secure XML processing is not supported", e); + //LOG.log(Level.WARNING, "Secure XML processing is not supported", e); } return factory; } @@ -60,7 +60,7 @@ public class DavDocumentBuilderFactory { * RFC 4918, Section 20.6 */ private static final EntityResolver DEFAULT_ENTITY_RESOLVER = (publicId, systemId) -> { - //LOG.debug("Resolution of external entities in XML payload not supported - publicId: " + publicId + ", systemId: " + //LOG.log(Level.FINE, "Resolution of external entities in XML payload not supported - publicId: " + publicId + ", systemId: " // + systemId); throw new IOException("This parser does not support resolution of external entities (publicId: " + publicId + ", systemId: " + systemId + ")"); diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/common/DavException.java b/files-webdav/src/main/java/org/xbib/io/webdav/common/DavException.java index 6e35f9d..6b775f5 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/common/DavException.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/common/DavException.java @@ -77,7 +77,7 @@ public class DavException extends IOException implements XMLizable { super(message, cause); this.errorCode = errorCode; this.errorCondition = errorCondition; - //log.debug("DavException: (" + errorCode + ") " + message); + //LOG.log(Level.FINE, "DavException: (" + errorCode + ") " + message); } /** diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/common/DavPropertyNameSet.java b/files-webdav/src/main/java/org/xbib/io/webdav/common/DavPropertyNameSet.java index b5e8b7d..55c6a8d 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/common/DavPropertyNameSet.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/common/DavPropertyNameSet.java @@ -126,7 +126,7 @@ public class DavPropertyNameSet extends PropContainer if (contentEntry instanceof DavPropertyName) { return add((DavPropertyName) contentEntry); } - //log.debug("DavPropertyName object expected. Found: " + contentEntry.getClass().toString()); + //LOG.log(Level.FINE, "DavPropertyName object expected. Found: " + contentEntry.getClass().toString()); return false; } diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/common/DavPropertySet.java b/files-webdav/src/main/java/org/xbib/io/webdav/common/DavPropertySet.java index 5ea209d..4026d11 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/common/DavPropertySet.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/common/DavPropertySet.java @@ -162,7 +162,7 @@ public class DavPropertySet extends PropContainer add((DavProperty) contentEntry); return true; } - //log.debug("DavProperty object expected. Found: " + contentEntry.getClass().toString()); + //LOG.log(Level.FINE, "DavProperty object expected. Found: " + contentEntry.getClass().toString()); return false; } diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/common/IfHeader.java b/files-webdav/src/main/java/org/xbib/io/webdav/common/IfHeader.java index 6c59114..85515db 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/common/IfHeader.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/common/IfHeader.java @@ -154,7 +154,7 @@ public class IfHeader implements Header { */ public boolean matches(String tag, String token, String etag) { if (ifHeader == null) { - //log.debug("matches: No If header, assume match"); + //LOG.log(Level.FINE, "matches: No If header, assume match"); return true; } else { return ifHeader.matches(tag, token, etag); @@ -215,7 +215,7 @@ public class IfHeader implements Header { } } else { - //log.debug("IfHeader: No If header in request"); + //LOG.log(Level.FINE, "IfHeader: No If header in request"); ifHeader = null; } return ifHeader; @@ -374,7 +374,7 @@ public class IfHeader implements Header { case ')': // correct end of list, end the loop - //log.debug("parseIfList: End of If list, terminating loop"); + //LOG.log(Level.FINE, "parseIfList: End of If list, terminating loop"); break ReadLoop; default: @@ -470,7 +470,7 @@ public class IfHeader implements Header { // catch up if a reader is given if (reader != null && effChar >= 0) { try { - //log.debug("logIllegalState: Catch up to any of "+expChar); + //LOG.log(Level.FINE, "logIllegalState: Catch up to any of "+expChar); do { reader.mark(1); effChar = reader.read(); diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/common/OrderPatch.java b/files-webdav/src/main/java/org/xbib/io/webdav/common/OrderPatch.java index ef53497..c6a2b0c 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/common/OrderPatch.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/common/OrderPatch.java @@ -94,7 +94,7 @@ public class OrderPatch implements XMLizable { */ public static OrderPatch createFromXml(Element orderPatchElement) throws DavException { if (!DomUtil.matches(orderPatchElement, "orderpatch", DavConstants.NAMESPACE)) { - //log.warn("ORDERPATH request body must start with an 'orderpatch' element."); + //LOG.log(Level.WARNING, "ORDERPATH request body must start with an 'orderpatch' element."); throw new DavException(400); } @@ -104,7 +104,7 @@ public class OrderPatch implements XMLizable { if (otype != null) { orderingType = DomUtil.getChildText(otype, DavConstants.XML_HREF, DavConstants.NAMESPACE); } else { - //log.warn("ORDERPATH request body must contain an 'ordering-type' child element."); + //LOG.log(Level.WARNING, "ORDERPATH request body must contain an 'ordering-type' child element."); throw new DavException(400); } @@ -121,7 +121,7 @@ public class OrderPatch implements XMLizable { Member om = new Member(segment, pos); tmpList.add(om); } catch (IllegalArgumentException e) { - //log.warn("Invalid element in 'orderpatch' request body: " + e.getMessage()); + //LOG.log(Level.WARNING, "Invalid element in 'orderpatch' request body: " + e.getMessage()); throw new DavException(400); } } diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/common/ParentElement.java b/files-webdav/src/main/java/org/xbib/io/webdav/common/ParentElement.java index fc76058..48af461 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/common/ParentElement.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/common/ParentElement.java @@ -36,7 +36,7 @@ public class ParentElement implements XMLizable { */ public static ParentElement createFromXml(Element root) throws DavException { if (!DomUtil.matches(root, "parent", DavConstants.NAMESPACE)) { - //log.warn("DAV:paret element expected"); + //LOG.log(Level.WARNING, "DAV:paret element expected"); throw new DavException(400); } String href = null; @@ -48,27 +48,27 @@ public class ParentElement implements XMLizable { if (segment == null) { segment = DomUtil.getText(elt); } else { - //log.warn("unexpected multiple occurrence of DAV:segment element"); + //LOG.log(Level.WARNING, "unexpected multiple occurrence of DAV:segment element"); throw new DavException(400); } } else if (DomUtil.matches(elt, "href", DavConstants.NAMESPACE)) { if (href == null) { href = DomUtil.getText(elt); } else { - //log.warn("unexpected multiple occurrence of DAV:href element"); + //LOG.log(Level.WARNING, "unexpected multiple occurrence of DAV:href element"); throw new DavException(400); } } else { - //log.warn("unexpected element " + elt.getLocalName()); + //LOG.log(Level.WARNING, "unexpected element " + elt.getLocalName()); throw new DavException(400); } } if (href == null) { - // log.warn("DAV:href element expected"); + // LOG.log(Level.WARNING, "DAV:href element expected"); throw new DavException(400); } if (segment == null) { - // log.warn("DAV:segment element expected"); + // LOG.log(Level.WARNING, "DAV:segment element expected"); throw new DavException(400); } return new ParentElement(href, segment); diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/common/Status.java b/files-webdav/src/main/java/org/xbib/io/webdav/common/Status.java index 233b553..ef770a5 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/common/Status.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/common/Status.java @@ -60,12 +60,12 @@ public class Status implements DavConstants, XMLizable { ++start; } if (!"HTTP".equals(statusLine.substring(at, at += 4))) { - //log.warn("Status-Line '" + statusLine + "' does not start with HTTP"); + //LOG.log(Level.WARNING, "Status-Line '" + statusLine + "' does not start with HTTP"); } //handle the HTTP-Version at = statusLine.indexOf(' ', at); if (at <= 0) { - // log.warn("Unable to parse HTTP-Version from the status line: '" + statusLine + "'"); + // LOG.log(Level.WARNING, "Unable to parse HTTP-Version from the status line: '" + statusLine + "'"); } String version = (statusLine.substring(start, at)).toUpperCase(); //advance through spaces diff --git a/files-webdav/src/main/java/org/xbib/io/webdav/common/SubscriptionInfo.java b/files-webdav/src/main/java/org/xbib/io/webdav/common/SubscriptionInfo.java index 1db0976..abc1d74 100644 --- a/files-webdav/src/main/java/org/xbib/io/webdav/common/SubscriptionInfo.java +++ b/files-webdav/src/main/java/org/xbib/io/webdav/common/SubscriptionInfo.java @@ -91,18 +91,18 @@ public class SubscriptionInfo implements XMLizable { */ public SubscriptionInfo(Element reqInfo, long timeout, boolean isDeep) throws DavException { if (!DomUtil.matches(reqInfo, "subscriptioninfo", DCR_NAMESPACE)) { - //log.warn("Element with name 'subscriptioninfo' expected"); + //LOG.log(Level.WARNING, "Element with name 'subscriptioninfo' expected"); throw new DavException(400); } Element el = DomUtil.getChildElement(reqInfo, "eventtype", DCR_NAMESPACE); if (el != null) { eventTypes = DefaultEventType.createFromXml(el); if (eventTypes.length == 0) { - //log.warn("'subscriptioninfo' must at least indicate a single, valid event type."); + //LOG.log(Level.WARNING, "'subscriptioninfo' must at least indicate a single, valid event type."); throw new DavException(400); } } else { - //log.warn("'subscriptioninfo' must contain an 'eventtype' child element."); + //LOG.log(Level.WARNING, "'subscriptioninfo' must contain an 'eventtype' child element."); throw new DavException(400); } diff --git a/gradle.properties b/gradle.properties index c48e4a0..679ab81 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ group = org.xbib name = files -version = 4.3.0 +version = 4.4.0 diff --git a/gradle/quality/pmd/category/java/bestpractices.xml b/gradle/quality/pmd/category/java/bestpractices.xml index 6bf15a0..56a764d 100644 --- a/gradle/quality/pmd/category/java/bestpractices.xml +++ b/gradle/quality/pmd/category/java/bestpractices.xml @@ -477,7 +477,7 @@ for (int i = 0, j = 0; i < 10; i++, j += 2) { diff --git a/settings.gradle b/settings.gradle index 2ae4f73..cd68f03 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,7 +28,6 @@ dependencyResolutionManagement { library('junit-jupiter-platform-launcher', 'org.junit.platform', 'junit-platform-launcher').version('1.10.1') library('hamcrest', 'org.hamcrest', 'hamcrest-library').version('2.2') library('junit4', 'junit', 'junit').version('4.13.2') - library('mockftpserver', 'org.mockftpserver', 'MockFtpServer').version('3.1.0') library('mockito-core', 'org.mockito', 'mockito-core').version('5.11.0') library('mockito-junit-jupiter', 'org.mockito', 'mockito-junit-jupiter').version('5.11.0') library('slf4j', 'org.slf4j', 'slf4j-api').version('2.0.13') @@ -39,6 +38,7 @@ dependencyResolutionManagement { include 'files-api' include 'files-ftp' include 'files-ftp-fs' +include 'files-ftp-mock' include 'files-sftp' include 'files-sftp-fs' include 'files-webdav'