diff --git a/.gitignore b/.gitignore index 31dd1c8..20f0307 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /*/out /*/.idea/workspace.xml /*/.idea/tasks.xml -/*/build +/**/build +/**/logs /*/.gradle diff --git a/.travis.yml b/.travis.yml index cc945a4..dc6c09c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ jdk: env: - PROJECT_DIR=01-Lazy + - PROJECT_DIR=04-SimpleFTP before_install: cd $PROJECT_DIR diff --git a/04-SimpleFTP/.idea/compiler.xml b/04-SimpleFTP/.idea/compiler.xml new file mode 100644 index 0000000..16653db --- /dev/null +++ b/04-SimpleFTP/.idea/compiler.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/gradle.xml b/04-SimpleFTP/.idea/gradle.xml new file mode 100644 index 0000000..16d8f01 --- /dev/null +++ b/04-SimpleFTP/.idea/gradle.xml @@ -0,0 +1,24 @@ + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/inspectionProfiles/Project_Default.xml b/04-SimpleFTP/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..a3e7ede --- /dev/null +++ b/04-SimpleFTP/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/libraries/Gradle__junit_junit_4_11.xml b/04-SimpleFTP/.idea/libraries/Gradle__junit_junit_4_11.xml new file mode 100644 index 0000000..dc26b34 --- /dev/null +++ b/04-SimpleFTP/.idea/libraries/Gradle__junit_junit_4_11.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/libraries/Gradle__org_apache_commons_commons_lang3_3_5.xml b/04-SimpleFTP/.idea/libraries/Gradle__org_apache_commons_commons_lang3_3_5.xml new file mode 100644 index 0000000..fed9b56 --- /dev/null +++ b/04-SimpleFTP/.idea/libraries/Gradle__org_apache_commons_commons_lang3_3_5.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/libraries/Gradle__org_apache_logging_log4j_log4j_api_2_8_2.xml b/04-SimpleFTP/.idea/libraries/Gradle__org_apache_logging_log4j_log4j_api_2_8_2.xml new file mode 100644 index 0000000..a183c4e --- /dev/null +++ b/04-SimpleFTP/.idea/libraries/Gradle__org_apache_logging_log4j_log4j_api_2_8_2.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/libraries/Gradle__org_apache_logging_log4j_log4j_core_2_8_2.xml b/04-SimpleFTP/.idea/libraries/Gradle__org_apache_logging_log4j_log4j_core_2_8_2.xml new file mode 100644 index 0000000..a2162fa --- /dev/null +++ b/04-SimpleFTP/.idea/libraries/Gradle__org_apache_logging_log4j_log4j_core_2_8_2.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/libraries/Gradle__org_hamcrest_hamcrest_core_1_3.xml b/04-SimpleFTP/.idea/libraries/Gradle__org_hamcrest_hamcrest_core_1_3.xml new file mode 100644 index 0000000..8262f72 --- /dev/null +++ b/04-SimpleFTP/.idea/libraries/Gradle__org_hamcrest_hamcrest_core_1_3.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/libraries/Gradle__org_hamcrest_hamcrest_library_1_3.xml b/04-SimpleFTP/.idea/libraries/Gradle__org_hamcrest_hamcrest_library_1_3.xml new file mode 100644 index 0000000..59eb8d5 --- /dev/null +++ b/04-SimpleFTP/.idea/libraries/Gradle__org_hamcrest_hamcrest_library_1_3.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/libraries/Gradle__org_jetbrains_annotations_15_0.xml b/04-SimpleFTP/.idea/libraries/Gradle__org_jetbrains_annotations_15_0.xml new file mode 100644 index 0000000..ca78bcd --- /dev/null +++ b/04-SimpleFTP/.idea/libraries/Gradle__org_jetbrains_annotations_15_0.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/misc.xml b/04-SimpleFTP/.idea/misc.xml new file mode 100644 index 0000000..2c55e21 --- /dev/null +++ b/04-SimpleFTP/.idea/misc.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules.xml b/04-SimpleFTP/.idea/modules.xml new file mode 100644 index 0000000..3930bb9 --- /dev/null +++ b/04-SimpleFTP/.idea/modules.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/04-SimpleFTP.iml b/04-SimpleFTP/.idea/modules/04-SimpleFTP.iml new file mode 100644 index 0000000..58f339a --- /dev/null +++ b/04-SimpleFTP/.idea/modules/04-SimpleFTP.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/04-SimpleFTP_main.iml b/04-SimpleFTP/.idea/modules/04-SimpleFTP_main.iml new file mode 100644 index 0000000..3fe6597 --- /dev/null +++ b/04-SimpleFTP/.idea/modules/04-SimpleFTP_main.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/04-SimpleFTP_test.iml b/04-SimpleFTP/.idea/modules/04-SimpleFTP_test.iml new file mode 100644 index 0000000..49bbaa4 --- /dev/null +++ b/04-SimpleFTP/.idea/modules/04-SimpleFTP_test.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/client/client.iml b/04-SimpleFTP/.idea/modules/client/client.iml new file mode 100644 index 0000000..3d5d201 --- /dev/null +++ b/04-SimpleFTP/.idea/modules/client/client.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/client/client_main.iml b/04-SimpleFTP/.idea/modules/client/client_main.iml new file mode 100644 index 0000000..77644d7 --- /dev/null +++ b/04-SimpleFTP/.idea/modules/client/client_main.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/client/client_test.iml b/04-SimpleFTP/.idea/modules/client/client_test.iml new file mode 100644 index 0000000..1a62050 --- /dev/null +++ b/04-SimpleFTP/.idea/modules/client/client_test.iml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/console-client/console-client.iml b/04-SimpleFTP/.idea/modules/console-client/console-client.iml new file mode 100644 index 0000000..90edea0 --- /dev/null +++ b/04-SimpleFTP/.idea/modules/console-client/console-client.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/console-client/console-client_main.iml b/04-SimpleFTP/.idea/modules/console-client/console-client_main.iml new file mode 100644 index 0000000..87dc98b --- /dev/null +++ b/04-SimpleFTP/.idea/modules/console-client/console-client_main.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/console-client/console-client_test.iml b/04-SimpleFTP/.idea/modules/console-client/console-client_test.iml new file mode 100644 index 0000000..df172d5 --- /dev/null +++ b/04-SimpleFTP/.idea/modules/console-client/console-client_test.iml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/graphic-client/graphic-client.iml b/04-SimpleFTP/.idea/modules/graphic-client/graphic-client.iml new file mode 100644 index 0000000..b7eb4ff --- /dev/null +++ b/04-SimpleFTP/.idea/modules/graphic-client/graphic-client.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/graphic-client/graphic-client_main.iml b/04-SimpleFTP/.idea/modules/graphic-client/graphic-client_main.iml new file mode 100644 index 0000000..b429cd9 --- /dev/null +++ b/04-SimpleFTP/.idea/modules/graphic-client/graphic-client_main.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/graphic-client/graphic-client_test.iml b/04-SimpleFTP/.idea/modules/graphic-client/graphic-client_test.iml new file mode 100644 index 0000000..fe4803a --- /dev/null +++ b/04-SimpleFTP/.idea/modules/graphic-client/graphic-client_test.iml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/integration-tests/integration-tests.iml b/04-SimpleFTP/.idea/modules/integration-tests/integration-tests.iml new file mode 100644 index 0000000..220cdcc --- /dev/null +++ b/04-SimpleFTP/.idea/modules/integration-tests/integration-tests.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/integration-tests/integration-tests_main.iml b/04-SimpleFTP/.idea/modules/integration-tests/integration-tests_main.iml new file mode 100644 index 0000000..cb510bf --- /dev/null +++ b/04-SimpleFTP/.idea/modules/integration-tests/integration-tests_main.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/integration-tests/integration-tests_test.iml b/04-SimpleFTP/.idea/modules/integration-tests/integration-tests_test.iml new file mode 100644 index 0000000..cdcd275 --- /dev/null +++ b/04-SimpleFTP/.idea/modules/integration-tests/integration-tests_test.iml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/messages/messages.iml b/04-SimpleFTP/.idea/modules/messages/messages.iml new file mode 100644 index 0000000..b0bf3ec --- /dev/null +++ b/04-SimpleFTP/.idea/modules/messages/messages.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/messages/messages_main.iml b/04-SimpleFTP/.idea/modules/messages/messages_main.iml new file mode 100644 index 0000000..68211ca --- /dev/null +++ b/04-SimpleFTP/.idea/modules/messages/messages_main.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/messages/messages_test.iml b/04-SimpleFTP/.idea/modules/messages/messages_test.iml new file mode 100644 index 0000000..a682490 --- /dev/null +++ b/04-SimpleFTP/.idea/modules/messages/messages_test.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/server/server.iml b/04-SimpleFTP/.idea/modules/server/server.iml new file mode 100644 index 0000000..d2d5ae8 --- /dev/null +++ b/04-SimpleFTP/.idea/modules/server/server.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/server/server_main.iml b/04-SimpleFTP/.idea/modules/server/server_main.iml new file mode 100644 index 0000000..1e7187a --- /dev/null +++ b/04-SimpleFTP/.idea/modules/server/server_main.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/modules/server/server_test.iml b/04-SimpleFTP/.idea/modules/server/server_test.iml new file mode 100644 index 0000000..b93864e --- /dev/null +++ b/04-SimpleFTP/.idea/modules/server/server_test.iml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/preferred-vcs.xml b/04-SimpleFTP/.idea/preferred-vcs.xml new file mode 100644 index 0000000..848cfc4 --- /dev/null +++ b/04-SimpleFTP/.idea/preferred-vcs.xml @@ -0,0 +1,6 @@ + + + + ApexVCS + + \ No newline at end of file diff --git a/04-SimpleFTP/.idea/uiDesigner.xml b/04-SimpleFTP/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/04-SimpleFTP/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/04-SimpleFTP/build.gradle b/04-SimpleFTP/build.gradle new file mode 100644 index 0000000..d590e0b --- /dev/null +++ b/04-SimpleFTP/build.gradle @@ -0,0 +1,14 @@ +group 'ru.spbau.bachelor2015.veselov.hw04' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.11' +} diff --git a/04-SimpleFTP/client/build.gradle b/04-SimpleFTP/client/build.gradle new file mode 100644 index 0000000..fe25e74 --- /dev/null +++ b/04-SimpleFTP/client/build.gradle @@ -0,0 +1,29 @@ +group 'ru.spbau.bachelor2015.veselov.hw04' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile project(":messages") + + testCompile group: 'junit', name: 'junit', version: '4.11' + + compile group: 'org.jetbrains', name: 'annotations', version: '15.0' + compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.8.2' +} + +jar { + manifest { + attributes 'Main-Class': 'ru.spbau.bachelor2015.veselov.hw04.Main' + } + + from { + configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + } +} \ No newline at end of file diff --git a/04-SimpleFTP/client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/client/Client.java b/04-SimpleFTP/client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/client/Client.java new file mode 100644 index 0000000..688929d --- /dev/null +++ b/04-SimpleFTP/client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/client/Client.java @@ -0,0 +1,213 @@ +package ru.spbau.bachelor2015.veselov.hw04.client; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import ru.spbau.bachelor2015.veselov.hw04.client.exceptions.ConnectionWasClosedException; +import ru.spbau.bachelor2015.veselov.hw04.messages.FTPGetMessage; +import ru.spbau.bachelor2015.veselov.hw04.messages.FTPListAnswerMessage; +import ru.spbau.bachelor2015.veselov.hw04.messages.FTPListMessage; +import ru.spbau.bachelor2015.veselov.hw04.messages.FTPMessage; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.*; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions.InvalidMessageException; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions.MessageNotReadException; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.nio.file.Path; +import java.util.List; + +/** + * A client class establishes a connection with ftp server and allows to send a request messages to it and receive + * responses. + */ +public class Client implements AutoCloseable { + private final static @NotNull Logger logger = LogManager.getLogger(Client.class.getCanonicalName()); + + private final @NotNull SocketChannel channel; + + private final @NotNull Selector selector; + + private final @NotNull FTPMessageReader messageReader; + + /** + * Connects to a server, makes a list request and immediately closes the connection. + * + * @param address address of server to connect to. + * @param path a path to a folder which content is requested. + * @return a list of entries which denotes a requested content. + * @throws IOException if any IO exception occurs during client interaction with a server. + * @throws ConnectionWasClosedException if in the middle of the process connection was closed. + */ + public static @NotNull List list(final @NotNull InetSocketAddress address, final @NotNull Path path) + throws IOException, ConnectionWasClosedException { + try (Client client = new Client(address.getHostName(), address.getPort())) { + return client.list(path); + } + } + + /** + * Connects to a server, makes a get request and immediately closes the connection. + * + * @param address address of server to connect to. + * @param pathToSource a path to a file which content is requested. + * @param pathToDestination a path to a file in which downloaded data will be written. + * @throws IOException if any IO exception occurs during client interaction with a server. + * @throws ConnectionWasClosedException if in the middle of the process connection was closed. + */ + public static void get(final @NotNull InetSocketAddress address, + final @NotNull Path pathToSource, + final @NotNull Path pathToDestination) + throws IOException, ConnectionWasClosedException { + try (Client client = new Client(address.getHostName(), address.getPort())) { + client.get(pathToSource, pathToDestination); + } + } + + /** + * Establishes a connection. + * + * @param host a host to connect to. + * @param port a port to connect to. + * @throws IOException if any IO exception occurs during establishment of the connection. + */ + public Client(final @NotNull String host, final int port) throws IOException { + selector = Selector.open(); + + channel = SocketChannel.open(); + channel.connect(new InetSocketAddress(host, port)); + channel.configureBlocking(false); + + channel.register(selector, SelectionKey.OP_READ); + + messageReader = new FTPMessageReader(channel); + + logger.info("A new client has established a connection with a server on {}:{}", host, port); + } + + /** + * Closes the connection. + * + * @throws IOException if any IO exception occurs during closing of connection. + */ + @Override + public void close() throws IOException { + channel.close(); + selector.close(); + } + + /** + * Performs a list request. This request asks a content of a specified folder. + * + * @param path a path to a folder which content is requested. + * @return a list of entries which denotes a requested content. + * @throws IOException if any IO exception occurs during client interaction with a server. + * @throws ConnectionWasClosedException if in the middle of the process connection was closed. + */ + public @NotNull List list(final @NotNull Path path) + throws IOException, ConnectionWasClosedException { + writeMessage(new FTPListMessage(path)); + + read(messageReader); + + FTPMessage answer; + + try { + answer = messageReader.getMessage(); + } catch (MessageNotReadException e) { + throw new RuntimeException(e); + } + + messageReader.reset(); + + logger.info("Client has received a message from server"); + + return ((FTPListAnswerMessage) answer).getContent(); + } + + /** + * Performs a get request. This request asks a content of a specified file. + * + * @param pathToSource a path to a file which content is requested. + * @param pathToDestination a path to a file in which downloaded data will be written. + * @throws IOException if any IO exception occurs during client interaction with a server. + * @throws ConnectionWasClosedException if in the middle of the process connection was closed. + */ + public void get(final @NotNull Path pathToSource, final @NotNull Path pathToDestination) + throws IOException, ConnectionWasClosedException { + writeMessage(new FTPGetMessage(pathToSource)); + + FileReceiver receiver = new FileReceiver(channel, pathToDestination); + read(receiver); + + logger.info("Client has received a file from server"); + } + + private void writeMessage(final @NotNull FTPMessage message) + throws IOException, ConnectionWasClosedException { + FTPMessageWriter writer = new FTPMessageWriter(channel, message); + + SelectionKey key = channel.keyFor(selector); + key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); + + boolean shouldRun = true; + while (shouldRun) { + selector.select(); + + if (key.isReadable()) { + int bytesRead = channel.read(ByteBuffer.allocate(1)); + + close(); + + if (bytesRead == -1) { + throw new ConnectionWasClosedException(); + } else { + throw new InvalidMessageException(); + } + } + + if (key.isWritable()) { + if (writer.write()) { + shouldRun = false; + } + } + + selector.selectedKeys().clear(); + } + + key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE); + logger.info("Client has sent a message to server"); + } + + private void read(final @NotNull DataReader reader) + throws IOException, ConnectionWasClosedException { + boolean shouldRun = true; + while (shouldRun) { + selector.select(); + + try { + switch (reader.read()) { + case NOT_READ: + break; + + case READ: + shouldRun = false; + break; + + case CLOSED: + throw new ConnectionWasClosedException(); + } + } catch (InvalidMessageException | ConnectionWasClosedException e) { + channel.close(); + + throw e; + } + + selector.selectedKeys().clear(); + } + } +} diff --git a/04-SimpleFTP/client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/client/exceptions/ConnectionWasClosedException.java b/04-SimpleFTP/client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/client/exceptions/ConnectionWasClosedException.java new file mode 100644 index 0000000..df0b006 --- /dev/null +++ b/04-SimpleFTP/client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/client/exceptions/ConnectionWasClosedException.java @@ -0,0 +1,6 @@ +package ru.spbau.bachelor2015.veselov.hw04.client.exceptions; + +/** + * An exception which might be thrown by Client if a connection was suddenly closed. + */ +public class ConnectionWasClosedException extends Exception {} diff --git a/04-SimpleFTP/client/src/main/resources/log4j2.xml b/04-SimpleFTP/client/src/main/resources/log4j2.xml new file mode 100644 index 0000000..6ad75d3 --- /dev/null +++ b/04-SimpleFTP/client/src/main/resources/log4j2.xml @@ -0,0 +1,23 @@ + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + diff --git a/04-SimpleFTP/console-client/build.gradle b/04-SimpleFTP/console-client/build.gradle new file mode 100644 index 0000000..28c33ce --- /dev/null +++ b/04-SimpleFTP/console-client/build.gradle @@ -0,0 +1,29 @@ +group 'ru.spbau.bachelor2015.veselov.hw04' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile project(":client") + + testCompile group: 'junit', name: 'junit', version: '4.11' + + compile group: 'org.jetbrains', name: 'annotations', version: '15.0' + compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.8.2' +} + +jar { + manifest { + attributes 'Main-Class': 'ru.spbau.bachelor2015.veselov.hw04.Main' + } + + from { + configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + } +} \ No newline at end of file diff --git a/04-SimpleFTP/console-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/cclient/Main.java b/04-SimpleFTP/console-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/cclient/Main.java new file mode 100644 index 0000000..65341bc --- /dev/null +++ b/04-SimpleFTP/console-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/cclient/Main.java @@ -0,0 +1,89 @@ +package ru.spbau.bachelor2015.veselov.hw04.cclient; + +import ru.spbau.bachelor2015.veselov.hw04.client.Client; +import ru.spbau.bachelor2015.veselov.hw04.client.exceptions.ConnectionWasClosedException; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.FileEntry; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Paths; +import java.util.List; +import java.util.Scanner; + +/** + * Entry point of a client program. This class allows to make requests to server. + */ +public class Main { + /** + * Entry point. + * + * @param args two arguments are expected. First is a string representation of a host, second is port. Together they + * form an address of a server to which client will be connected. + * @throws IOException if any IO exception occurs during application work. + */ + public static void main(String[] args) throws IOException { + if (args.length != 2) { + System.out.println("Two arguments expected: "); + return; + } + + try (Client client = new Client(args[0], Integer.parseInt(args[1])); + InputStreamReader inputStreamReader = new InputStreamReader(System.in); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + boolean shouldRun = true; + while (shouldRun) { + String inputLine = bufferedReader.readLine(); + Scanner scanner = new Scanner(inputLine); + + if (!scanner.hasNext()) { + continue; + } + + String command = scanner.next(); + switch (command) { + case "disconnect": + shouldRun = false; + break; + + case "list": + if (!scanner.hasNext()) { + System.out.println("Argument expected: "); + break; + } + + List answer = client.list(Paths.get(scanner.next())); + for (FileEntry entry : answer) { + System.out.println(entry); + } + + break; + + case "get": + if (!scanner.hasNext()) { + System.out.println("Two arguments expected: "); + break; + } + + String source = scanner.next(); + + if (!scanner.hasNext()) { + System.out.println("Two arguments expected: "); + break; + } + + String destination = scanner.next(); + + client.get(Paths.get(source), Paths.get(destination)); + + break; + + default: + System.out.println("Unknown command: " + command); + } + } + } catch (ConnectionWasClosedException e) { + System.out.println("You were disconnected"); + } + } +} diff --git a/04-SimpleFTP/gradle/wrapper/gradle-wrapper.jar b/04-SimpleFTP/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..6ffa237 Binary files /dev/null and b/04-SimpleFTP/gradle/wrapper/gradle-wrapper.jar differ diff --git a/04-SimpleFTP/gradle/wrapper/gradle-wrapper.properties b/04-SimpleFTP/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..884f310 --- /dev/null +++ b/04-SimpleFTP/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Apr 23 19:22:48 MSK 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-bin.zip diff --git a/04-SimpleFTP/gradlew b/04-SimpleFTP/gradlew new file mode 100755 index 0000000..9aa616c --- /dev/null +++ b/04-SimpleFTP/gradlew @@ -0,0 +1,169 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/04-SimpleFTP/gradlew.bat b/04-SimpleFTP/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/04-SimpleFTP/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/04-SimpleFTP/graphic-client/build.gradle b/04-SimpleFTP/graphic-client/build.gradle new file mode 100644 index 0000000..278646c --- /dev/null +++ b/04-SimpleFTP/graphic-client/build.gradle @@ -0,0 +1,18 @@ +group 'ru.spbau.bachelor2015.veselov.hw04' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile project(":client") + + testCompile group: 'junit', name: 'junit', version: '4.11' + + compile group: 'org.jetbrains', name: 'annotations', version: '15.0' +} diff --git a/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ApplicationModel.java b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ApplicationModel.java new file mode 100644 index 0000000..a2b67bb --- /dev/null +++ b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ApplicationModel.java @@ -0,0 +1,154 @@ +package ru.spbau.bachelor2015.veselov.hw04.gclient; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.Alert; +import javafx.stage.Stage; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import ru.spbau.bachelor2015.veselov.hw04.client.Client; +import ru.spbau.bachelor2015.veselov.hw04.client.exceptions.ConnectionWasClosedException; +import ru.spbau.bachelor2015.veselov.hw04.gclient.exceptions.ServerAddressIsNotSetException; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.FileEntry; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.UnresolvedAddressException; +import java.nio.channels.UnsupportedAddressTypeException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * An application model class which represent a bare logic of a programme with minimum interface. + */ +public final class ApplicationModel { + private @NotNull Stage mainStage; + + private @Nullable InetSocketAddress serverAddress; + + private @Nullable Path currentFolder; + + private final @NotNull ObservableList currentFolderObservable = FXCollections.observableArrayList( + new FileEntryWrapper(new FileEntry(Paths.get("root/folder1"), true)), + new FileEntryWrapper(new FileEntry(Paths.get("root/folder2"), true)), + new FileEntryWrapper(new FileEntry(Paths.get("root/file1"), false)), + new FileEntryWrapper(new FileEntry(Paths.get("root/file2"), false)), + new FileEntryWrapper(new FileEntry(Paths.get("root/file3"), false)) + ); + + /** + * Creates an application model. + * + * @param mainStage a main stage of a programme. + */ + public ApplicationModel(final @NotNull Stage mainStage) { + this.mainStage = mainStage; + } + + /** + * Returns main stage. + */ + public @NotNull Stage getMainStage() { + return mainStage; + } + + /** + * Sets new server address. + * + * @param serverAddress a new server address. + */ + public void setServerAddress(final @NotNull InetSocketAddress serverAddress) { + loadFolderContent(serverAddress, Paths.get("")).ifPresent(entries -> { + this.serverAddress = serverAddress; + + currentFolderObservable.clear(); + currentFolderObservable.addAll(entries); + }); + } + + /** + * Sets new current folder which content will be displayed on the screen. + * + * @param path a path to new current folder. + * @throws ServerAddressIsNotSetException if server address is not set. + */ + public void setCurrentFolder(final @NotNull Path path) throws ServerAddressIsNotSetException { + if (serverAddress == null) { + throw new ServerAddressIsNotSetException(); + } + + loadFolderContent(serverAddress, path).ifPresent(entries -> { + currentFolder = path; + + currentFolderObservable.clear(); + + if (!path.equals(Paths.get(""))) { + Path parent = path.getParent(); + if (parent == null) { + parent = Paths.get(""); + } + + currentFolderObservable.add(new FileEntryWrapper(new FileEntry(parent, true), "..")); + } + + currentFolderObservable.addAll(entries); + }); + } + + /** + * Returns a special observable list which contains entries for current folder. + */ + public @NotNull ObservableList getCurrentFolderObservable() { + return currentFolderObservable; + } + + /** + * Downloads a file to a given destination. + * + * @param pathToSource a path to a source file on server. + * @param pathToDestination a local path to a destination where data will be written. + * @throws ServerAddressIsNotSetException if server address is not set. + */ + public void downloadFile(final @NotNull Path pathToSource, + final @NotNull Path pathToDestination) throws ServerAddressIsNotSetException{ + if (serverAddress == null) { + throw new ServerAddressIsNotSetException(); + } + + try { + Client.get(serverAddress, pathToSource, pathToDestination); + } catch (IOException | + UnresolvedAddressException | + UnsupportedAddressTypeException | + ConnectionWasClosedException e) { + showFailureAlert(); + } + } + + private @NotNull Optional> loadFolderContent(final @NotNull InetSocketAddress serverAddress, + final @NotNull Path folder) { + try { + return Optional.of(Client.list(serverAddress, folder).stream() + .map(FileEntryWrapper::new) + .collect(Collectors.toList())); + } catch (IOException | + UnresolvedAddressException | + UnsupportedAddressTypeException | + ConnectionWasClosedException e) { + showFailureAlert(); + + return Optional.empty(); + } + } + + private void showFailureAlert() { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText("A connection failure"); + alert.setContentText("Failed to download data from given server address."); + alert.showAndWait(); + } +} diff --git a/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ClientApplication.java b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ClientApplication.java new file mode 100644 index 0000000..e3e212c --- /dev/null +++ b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ClientApplication.java @@ -0,0 +1,36 @@ +package ru.spbau.bachelor2015.veselov.hw04.gclient; + +import javafx.application.Application; +import javafx.stage.Stage; +import org.jetbrains.annotations.NotNull; +import ru.spbau.bachelor2015.veselov.hw04.gclient.ui.MainSceneProducer; + +/** + * A main class which is an entry point of this application. + */ +public class ClientApplication extends Application { + /** + * This method is called by JavaFX when all initialization has been done. + * + * @param primaryStage a primary stage of this application. + * @throws Exception any uncaught exception. + */ + @Override + public void start(final @NotNull Stage primaryStage) throws Exception { + primaryStage.setTitle("FTP client"); + primaryStage.setWidth(800); + primaryStage.setHeight(600); + + primaryStage.setScene(MainSceneProducer.produce(new ApplicationModel(primaryStage))); + primaryStage.show(); + } + + /** + * Entry point of a programme. + * + * @param args arguments are passed to JavaFX library. + */ + public static void main(final @NotNull String[] args) { + launch(args); + } +} \ No newline at end of file diff --git a/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/FileEntryWrapper.java b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/FileEntryWrapper.java new file mode 100644 index 0000000..c9288ae --- /dev/null +++ b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/FileEntryWrapper.java @@ -0,0 +1,50 @@ +package ru.spbau.bachelor2015.veselov.hw04.gclient; + +import org.jetbrains.annotations.NotNull; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.FileEntry; + +/** + * A file entry wrapper class which adds a new String name field which allows to create a specific String representation + * of an entry for a list of entries on the screen. + */ +public class FileEntryWrapper { + private final @NotNull FileEntry entry; + + private final @NotNull String name; + + /** + * Creates a wrapper from file entry. This ctor uses real file entry name as string representation. + * + * @param entry an entry to create wrapper for. + */ + public FileEntryWrapper(final @NotNull FileEntry entry) { + this.entry = entry; + + name = entry.getFileName(); + } + + /** + * Creates a wrapper from file entry. + * + * @param entry an entry to create wrapper for. + * @param name a string represenation of an entry. + */ + public FileEntryWrapper(final @NotNull FileEntry entry, final @NotNull String name) { + this.entry = entry; + this.name = name; + } + + /** + * Returns underlying entry. + */ + public @NotNull FileEntry getEntry() { + return entry; + } + + /** + * Returns a string represenation of an entry. + */ + public @NotNull String getName() { + return name; + } +} diff --git a/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/exceptions/ServerAddressIsNotSetException.java b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/exceptions/ServerAddressIsNotSetException.java new file mode 100644 index 0000000..71cb54f --- /dev/null +++ b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/exceptions/ServerAddressIsNotSetException.java @@ -0,0 +1,7 @@ +package ru.spbau.bachelor2015.veselov.hw04.gclient.exceptions; + +/** + * This exception might be thrown by ApplicationModel class if some action which requires server address was invoked + * while server address hasn't been set. + */ +public class ServerAddressIsNotSetException extends Exception {} diff --git a/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/FileTableProducer.java b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/FileTableProducer.java new file mode 100644 index 0000000..e8f2a66 --- /dev/null +++ b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/FileTableProducer.java @@ -0,0 +1,86 @@ +package ru.spbau.bachelor2015.veselov.hw04.gclient.ui; + +import javafx.beans.binding.StringBinding; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableRow; +import javafx.scene.control.TableView; +import javafx.stage.FileChooser; +import org.jetbrains.annotations.NotNull; +import ru.spbau.bachelor2015.veselov.hw04.gclient.ApplicationModel; +import ru.spbau.bachelor2015.veselov.hw04.gclient.FileEntryWrapper; +import ru.spbau.bachelor2015.veselov.hw04.gclient.exceptions.ServerAddressIsNotSetException; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.FileEntry; + +import java.io.File; + +/** + * Special class which has only one method to produce table widget for current folder entries. + */ +public final class FileTableProducer { + /** + * Returns initialized widget. + * + * @param model an application model. + */ + public static @NotNull TableView produce(final @NotNull ApplicationModel model) { + TableView table = new TableView<>(); + + table.setEditable(false); + table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + + TableColumn fileNameColumn = new TableColumn<>("Name"); + fileNameColumn.setMinWidth(100); + fileNameColumn.setCellValueFactory(param -> new StringBinding() { + @Override + protected @NotNull String computeValue() { + return param.getValue().getName(); + } + }); + + TableColumn fileTypeColumn = new TableColumn<>("Type"); + fileTypeColumn.setMinWidth(100); + fileTypeColumn.setCellValueFactory(param -> new StringBinding() { + @Override + protected @NotNull String computeValue() { + return param.getValue().getEntry().isDirectory() ? "folder" : ""; + } + }); + + table.getColumns().addAll(fileNameColumn, fileTypeColumn); + + table.setRowFactory(param -> { + TableRow row = new TableRow<>(); + row.setOnMouseClicked(event -> { + if (event.getClickCount() != 2 || row.isEmpty()) { + return; + } + + FileEntry entry = row.getItem().getEntry(); + if (!entry.isDirectory()) { + FileChooser fileChooser = SaveAsFileChooserProducer.produce(entry.getFileName()); + File file = fileChooser.showSaveDialog(model.getMainStage()); + if (file == null) { + return; + } + + try { + model.downloadFile(row.getItem().getEntry().getPath(), file.toPath()); + } catch (ServerAddressIsNotSetException e) { + throw new RuntimeException(e); + } + } else { + try { + model.setCurrentFolder(row.getItem().getEntry().getPath()); + } catch (ServerAddressIsNotSetException e) { + throw new RuntimeException(e); + } + } + }); + + return row; + }); + + table.setItems(model.getCurrentFolderObservable()); + return table; + } +} diff --git a/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/MainSceneProducer.java b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/MainSceneProducer.java new file mode 100644 index 0000000..4463e0c --- /dev/null +++ b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/MainSceneProducer.java @@ -0,0 +1,32 @@ +package ru.spbau.bachelor2015.veselov.hw04.gclient.ui; + +import javafx.scene.Scene; +import javafx.scene.control.TableView; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.jetbrains.annotations.NotNull; +import ru.spbau.bachelor2015.veselov.hw04.gclient.ApplicationModel; + +/** + * Special class which has only one method to produce main scene widget. + */ +public final class MainSceneProducer { + /** + * Returns initialized widget. + * + * @param model an application model. + */ + public static @NotNull Scene produce(final @NotNull ApplicationModel model) { + TableView fileTable = FileTableProducer.produce(model); + + VBox vBox = new VBox(); + vBox.getChildren().addAll(ToolBarProducer.produce(model), fileTable); + + vBox.setFillWidth(true); + VBox.setVgrow(fileTable, Priority.ALWAYS); + + Scene scene = new Scene(vBox); + + return scene; + } +} diff --git a/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/SaveAsFileChooserProducer.java b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/SaveAsFileChooserProducer.java new file mode 100644 index 0000000..290637a --- /dev/null +++ b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/SaveAsFileChooserProducer.java @@ -0,0 +1,22 @@ +package ru.spbau.bachelor2015.veselov.hw04.gclient.ui; + +import javafx.stage.FileChooser; +import org.jetbrains.annotations.NotNull; + +/** + * Special class which has only one method to produce file chooser widget for file saving. + */ +public class SaveAsFileChooserProducer { + /** + * Returns initialized widget. + * + * @param fileName a default file name. + */ + public static @NotNull FileChooser produce(final @NotNull String fileName) { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Save as"); + fileChooser.setInitialFileName(fileName); + + return fileChooser; + } +} diff --git a/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/ServerChoiceDialogProducer.java b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/ServerChoiceDialogProducer.java new file mode 100644 index 0000000..bd508c9 --- /dev/null +++ b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/ServerChoiceDialogProducer.java @@ -0,0 +1,116 @@ +package ru.spbau.bachelor2015.veselov.hw04.gclient.ui; + +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.layout.GridPane; +import org.jetbrains.annotations.NotNull; + +import java.net.InetSocketAddress; + +/** + * Special class which has only one method to produce choice dialog widget. + */ +public final class ServerChoiceDialogProducer { + /** + * Returns initialized widget. + */ + public static @NotNull Dialog produce() { + Dialog dialog = new Dialog<>(); + dialog.setTitle("Server Choice Dialog"); + dialog.setHeaderText("Enter address of a server"); + + ButtonType chooseButtonType = new ButtonType("OK", ButtonBar.ButtonData.OK_DONE); + dialog.getDialogPane().getButtonTypes().addAll(chooseButtonType, ButtonType.CANCEL); + + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(10); + grid.setPadding(new Insets(20, 150, 10, 10)); + + Node chooseButton = dialog.getDialogPane().lookupButton(chooseButtonType); + + HostPortTextFields textFields = new HostPortTextFields(chooseButton); + + grid.add(new Label("Host:"), 0, 0); + grid.add(textFields.getHostTextField(), 1, 0); + grid.add(new Label("Port:"), 0, 1); + grid.add(textFields.getPortTextField(), 1, 1); + + dialog.getDialogPane().setContent(grid); + + Platform.runLater(() -> textFields.getHostTextField().requestFocus()); + + dialog.setResultConverter(dialogButton -> { + if (dialogButton == chooseButtonType) { + return new InetSocketAddress(textFields.getHostTextField().getText(), + Integer.parseInt(textFields.getPortTextField().getText())); + } + + return null; + }); + + return dialog; + } + + private static final class HostPortTextFields { + private final @NotNull TextField host; + + private final @NotNull TextField port; + + private final @NotNull Node chooseButton; + + private boolean doesHostMakeDisabled = true; + + private boolean doesPortMakeDisabled = true; + + public HostPortTextFields(final @NotNull Node chooseButton) { + host = new TextField(); + host.setPromptText("Host"); + + host.textProperty().addListener( + (observable, oldValue, newValue) -> { + doesHostMakeDisabled = newValue.isEmpty(); + chooseButton.setDisable(isDisabled()); + } + ); + + port = new TextField(); + port.setPromptText("Port"); + + port.textProperty().addListener( + (observable, oldValue, newValue) -> { + doesPortMakeDisabled = false; + + if (newValue.isEmpty()) { + doesPortMakeDisabled = true; + } else { + try { + Integer.parseInt(newValue); + } catch (NumberFormatException e) { + doesPortMakeDisabled = true; + } + } + + chooseButton.setDisable(isDisabled()); + } + ); + + this.chooseButton = chooseButton; + this.chooseButton.setDisable(true); + } + + public @NotNull TextField getHostTextField() { + return host; + } + + public @NotNull TextField getPortTextField() { + return port; + } + + private boolean isDisabled() { + return doesHostMakeDisabled || doesPortMakeDisabled; + } + } +} diff --git a/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/ToolBarProducer.java b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/ToolBarProducer.java new file mode 100644 index 0000000..aa16476 --- /dev/null +++ b/04-SimpleFTP/graphic-client/src/main/java/ru/spbau/bachelor2015/veselov/hw04/gclient/ui/ToolBarProducer.java @@ -0,0 +1,34 @@ +package ru.spbau.bachelor2015.veselov.hw04.gclient.ui; + +import javafx.scene.control.Button; +import javafx.scene.control.Dialog; +import javafx.scene.control.ToolBar; +import org.jetbrains.annotations.NotNull; +import ru.spbau.bachelor2015.veselov.hw04.gclient.ApplicationModel; + +import java.net.InetSocketAddress; +import java.util.Optional; + +/** + * Special class which has only one method to produce toolbar widget. + */ +public final class ToolBarProducer { + /** + * Returns initialized widget. + * + * @param model an application model. + */ + public static @NotNull ToolBar produce(final @NotNull ApplicationModel model) { + Dialog dialog = ServerChoiceDialogProducer.produce(); + + Button chooseServerButton = new Button("Choose server"); + chooseServerButton.setOnAction(event -> { + Optional result = dialog.showAndWait(); + result.ifPresent(model::setServerAddress); + }); + + ToolBar toolBar = new ToolBar(chooseServerButton); + + return toolBar; + } +} diff --git a/04-SimpleFTP/integration-tests/build.gradle b/04-SimpleFTP/integration-tests/build.gradle new file mode 100644 index 0000000..5f327b3 --- /dev/null +++ b/04-SimpleFTP/integration-tests/build.gradle @@ -0,0 +1,21 @@ +group 'ru.spbau.bachelor2015.veselov.hw04' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile project(":messages") + compile project(":server") + compile project(":client") + + testCompile group: 'org.hamcrest', name: 'hamcrest-library', version: '1.3' + testCompile group: 'junit', name: 'junit', version: '4.11' + + compile group: 'org.jetbrains', name: 'annotations', version: '15.0' +} \ No newline at end of file diff --git a/04-SimpleFTP/integration-tests/src/test/java/ru/spbau/bachelor2015/veselov/hw04/ServerTest.java b/04-SimpleFTP/integration-tests/src/test/java/ru/spbau/bachelor2015/veselov/hw04/ServerTest.java new file mode 100644 index 0000000..13426c8 --- /dev/null +++ b/04-SimpleFTP/integration-tests/src/test/java/ru/spbau/bachelor2015/veselov/hw04/ServerTest.java @@ -0,0 +1,208 @@ +package ru.spbau.bachelor2015.veselov.hw04; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.jetbrains.annotations.NotNull; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import ru.spbau.bachelor2015.veselov.hw04.client.Client; +import ru.spbau.bachelor2015.veselov.hw04.client.exceptions.ConnectionWasClosedException; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.FileEntry; +import ru.spbau.bachelor2015.veselov.hw04.server.Server; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class ServerTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private final int foldersInRootFolder = 10; + + private Path[] relativePathToFolderInRoot; + + private Path[] relativePathToFilesInRootSubFolders; + + private Path pathToTrackedFolder; + + private final int port = 10000; + + private Server server; + + private Client client; + + private void folderInitialization() throws Exception { + String trackedFolderName = "tracked"; + String folderNamePrefix = "dir"; + String fileNamePrefix = "file"; + + pathToTrackedFolder = temporaryFolder.newFolder(trackedFolderName).toPath(); + + relativePathToFolderInRoot = new Path[foldersInRootFolder]; + relativePathToFilesInRootSubFolders = new Path[foldersInRootFolder]; + + for (int i = 0; i < foldersInRootFolder; i++) { + Path pathToFolder = Files.createDirectory(pathToTrackedFolder.resolve(folderNamePrefix + i)); + Path pathToFile = Files.createFile(pathToFolder.resolve(fileNamePrefix + i)); + Files.write(pathToFile, new byte[] { (byte) i }); + + relativePathToFolderInRoot[i] = pathToTrackedFolder.relativize(pathToFolder); + relativePathToFilesInRootSubFolders[i] = pathToTrackedFolder.relativize(pathToFile); + } + } + + private void serverInitialization() { + server = new Server(pathToTrackedFolder, port); + server.start(); + } + + private void clientInitialization() { + client = null; + + while (client == null) { + try { + client = new Client("localhost", port); + } catch (IOException ignore) { + } + } + } + + @Before + public void before() throws Exception { + folderInitialization(); + serverInitialization(); + clientInitialization(); + } + + @After + public void after() throws IOException, InterruptedException { + client.close(); + server.stop(); + } + + @Test(timeout = 1000) + public void testFTPListMessage() throws Exception { + new SubDirTestMessage(0).test(client); + } + + @Test(timeout = 2000) + public void testFTPListMessages() throws Exception { + for (int i = 0; i < foldersInRootFolder; i++) { + new SubDirTestMessage(i).test(client); + } + } + + @Test(timeout = 1000) + public void testListRequestOfRootFolder() throws Exception { + List entries = this.client.list(pathToTrackedFolder.relativize(pathToTrackedFolder)); + + Matcher[] matchers = new Matcher[foldersInRootFolder]; + for (int i = 0; i < foldersInRootFolder; i++) { + matchers[i] = fileEntry(relativePathToFolderInRoot[i], true); + } + + assertThat(entries, containsInAnyOrder(matchers)); + } + + @Test(expected = ConnectionWasClosedException.class, timeout = 1000) + public void testListRequestOfForbiddenFolder() throws Exception { + client.list(pathToTrackedFolder.relativize(temporaryFolder.getRoot().toPath())); + } + + @Test(timeout = 1000) + public void testGetRequestOnSmallFile() throws Exception { + testGetOnFile(relativePathToFilesInRootSubFolders[0]); + } + + @Test(timeout = 4000) + public void testGetRequestOnBigFile() throws Exception { + final String fileName = "big-file"; + + Path pathToFile = pathToTrackedFolder.resolve(fileName); + + Files.createFile(pathToFile); + + byte[] data = new byte[1024 * 1024 * 10]; // 10 Mb + for (int i = 0; i < data.length; i++) { + data[i] = (byte) i; + } + + Files.write(pathToFile, data); + + testGetOnFile(pathToTrackedFolder.relativize(pathToFile)); + } + + @Test(expected = ConnectionWasClosedException.class, timeout = 1000) + public void serverDisconnects() throws Exception { + server.stop(); + + client.list(pathToTrackedFolder.relativize(pathToTrackedFolder)); + } + + private void testGetOnFile(final @NotNull Path pathToSource) throws Exception { + final Path pathToDestination = temporaryFolder.newFile().toPath(); + + client.get(pathToSource, pathToDestination); + + assertThat(Files.readAllBytes(pathToTrackedFolder.resolve(pathToSource)), + is(equalTo(Files.readAllBytes(pathToDestination)))); + } + + private @NotNull FileEntryMatcher fileEntry(final @NotNull Path path, + final boolean isDirectory) { + return new FileEntryMatcher(path, isDirectory); + } + + private class SubDirTestMessage { + private final int subDirIndex; + + public SubDirTestMessage(final int index) { + subDirIndex = index; + } + + public void test(final @NotNull Client client) throws Exception { + List answer = + client.list(relativePathToFolderInRoot[subDirIndex]); + + assertThat(answer, is(contains( + fileEntry(relativePathToFilesInRootSubFolders[subDirIndex], false)))); + } + } + + private static class FileEntryMatcher extends BaseMatcher { + private final @NotNull Path path; + + private final boolean isDirectory; + + public FileEntryMatcher(final @NotNull Path path, final boolean isDirectory) { + this.path = path; + this.isDirectory = isDirectory; + } + + @Override + public boolean matches(final @NotNull Object item) { + if (!(item instanceof FileEntry)) { + return false; + } + + FileEntry entry = (FileEntry) item; + + return entry.getPath().equals(path) && entry.isDirectory() == isDirectory; + } + + @Override + public void describeTo(final @NotNull Description description) { + description.appendText("expected path: ").appendValue(path) + .appendText(", expected isDirectory: ").appendValue(isDirectory); + } + } +} \ No newline at end of file diff --git a/04-SimpleFTP/messages/build.gradle b/04-SimpleFTP/messages/build.gradle new file mode 100644 index 0000000..e77f8de --- /dev/null +++ b/04-SimpleFTP/messages/build.gradle @@ -0,0 +1,17 @@ +group 'ru.spbau.bachelor2015.veselov.hw04' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.11' + + compile group: 'org.jetbrains', name: 'annotations', version: '15.0' + compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.5' +} \ No newline at end of file diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/FTPGetMessage.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/FTPGetMessage.java new file mode 100644 index 0000000..15fd553 --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/FTPGetMessage.java @@ -0,0 +1,29 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages; + +import org.jetbrains.annotations.NotNull; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.IndependentPath; + +import java.nio.file.Path; + +/** + * An ftp request message. This message asks server to send a content of a specified file. + */ +public class FTPGetMessage implements FTPMessage { + private final @NotNull IndependentPath path; + + /** + * Creates a message. + * + * @param path a path to a file which content is requested by this message. + */ + public FTPGetMessage(final @NotNull Path path) { + this.path = new IndependentPath(path); + } + + /** + * Returns a path to a file which content is requested by this message. + */ + public @NotNull Path getPath() { + return path.toPath(); + } +} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/FTPListAnswerMessage.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/FTPListAnswerMessage.java new file mode 100644 index 0000000..92206d5 --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/FTPListAnswerMessage.java @@ -0,0 +1,31 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages; + +import org.jetbrains.annotations.NotNull; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.FileEntry; + +import java.util.ArrayList; +import java.util.List; + +/** + * An ftp message which represents an answer on list request message. + */ +public class FTPListAnswerMessage implements FTPMessage { + private final @NotNull List content; + + /** + * Creates a message. + * + * @param content a list of entries which this message stores. + */ + public FTPListAnswerMessage(final @NotNull List content) { + this.content = new ArrayList<>(content); + } + + /** + * Returns a list of entries which this message stores. + */ + public @NotNull List getContent() { + return new ArrayList<>(content); + } + +} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/FTPListMessage.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/FTPListMessage.java new file mode 100644 index 0000000..4f94b29 --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/FTPListMessage.java @@ -0,0 +1,29 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages; + +import org.jetbrains.annotations.NotNull; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.IndependentPath; + +import java.nio.file.Path; + +/** + * A request ftp message. This message asks server to list the content of a specified folder. + */ +public class FTPListMessage implements FTPMessage { + private final @NotNull IndependentPath path; + + /** + * Creates a message. + * + * @param path a path to a folder which content is requested by this message. + */ + public FTPListMessage(final @NotNull Path path) { + this.path = new IndependentPath(path); + } + + /** + * Returns a path to a folder which content is requested by this message. + */ + public @NotNull Path getPath() { + return path.toPath(); + } +} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/FTPMessage.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/FTPMessage.java new file mode 100644 index 0000000..8c64060 --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/FTPMessage.java @@ -0,0 +1,13 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages; + +import java.io.Serializable; + +/** + * An interface which represents an abstract message which ftp server can use to interact on its' connections. + */ +public interface FTPMessage extends Serializable { + /** + * Maximal possible length of a message. A longer message considered to be invalid. + */ + int MAXIMAL_MESSAGE_LENGTH = 1024 * 1024; // 1 Mb +} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/DataReader.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/DataReader.java new file mode 100644 index 0000000..000a0c4 --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/DataReader.java @@ -0,0 +1,23 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages.util; + +import java.io.IOException; + +/** + * An interface which represents an entity which has an ability to read. It is assumed that such entity reads from a + * socket channel. + */ +public interface DataReader { + /** + * Makes an attempt to read data from data source and returns the result of reading. + * + * @throws IOException if any IO exception occurs during reading process. + */ + ReadingResult read() throws IOException; + + /** + * Possible results of reading operation. CLOSED means that peer has closed the connection. + */ + enum ReadingResult { + READ, NOT_READ, CLOSED + } +} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/DataWriter.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/DataWriter.java new file mode 100644 index 0000000..2a6af1d --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/DataWriter.java @@ -0,0 +1,17 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages.util; + +import java.io.IOException; + +/** + * An interface which represents an entity which has an ability to write. It is assumed that such entity writes to a + * socket channel. + */ +public interface DataWriter { + /** + * Makes an attempt to write data to data consumer and returns the result of writing which is true in case when + * writer has written all the data it had. + * + * @throws IOException if any IO exception occurs during writing process. + */ + boolean write() throws IOException; +} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FTPMessageReader.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FTPMessageReader.java new file mode 100644 index 0000000..a4d937f --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FTPMessageReader.java @@ -0,0 +1,100 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages.util; + +import org.apache.commons.lang3.SerializationUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import ru.spbau.bachelor2015.veselov.hw04.messages.FTPMessage; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions.LongMessageException; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions.MessageNotReadException; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions.MessageWithNonpositiveLengthException; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; + +/** + * Message reader is a data reader which is responsible for reading a single ftp message from a specified channel. + */ +public class FTPMessageReader implements DataReader { + private final @NotNull ReadableByteChannel channel; + + private boolean isLengthRead = false; + + private @NotNull final ByteBuffer lengthBuffer = ByteBuffer.allocate(Integer.BYTES); + + private @Nullable ByteBuffer messageBuffer; + + /** + * Creates a message reader for a specified channel. + * + * @param channel a channel which this reader will be reading from. + */ + public FTPMessageReader(final @NotNull ReadableByteChannel channel) { + this.channel = channel; + } + + /** + * Makes an attempt to read a message. + * + * @return the result of current attempt. + * @throws IOException if any IO exception occurs during reading process or if an invalid message was received. + */ + public DataReader.ReadingResult read() throws IOException { + if (!isLengthRead) { + if (channel.read(lengthBuffer) == -1) { + return DataReader.ReadingResult.CLOSED; + } + + if (lengthBuffer.hasRemaining()) { + return DataReader.ReadingResult.NOT_READ; + } + + isLengthRead = true; + + lengthBuffer.flip(); + int length = lengthBuffer.getInt(); + + if (length <= 0) { + throw new MessageWithNonpositiveLengthException(); + } + + if (length > FTPMessage.MAXIMAL_MESSAGE_LENGTH) { + throw new LongMessageException(); + } + + messageBuffer = ByteBuffer.allocate(length); + } + + if (channel.read(messageBuffer) == -1) { + return DataReader.ReadingResult.CLOSED; + } + + if (messageBuffer.hasRemaining()) { + return DataReader.ReadingResult.NOT_READ; + } + + return DataReader.ReadingResult.READ; + } + + /** + * Returns a read ftp message. + * + * @throws MessageNotReadException if the message hasn't been read yet. + */ + public @NotNull FTPMessage getMessage() throws MessageNotReadException { + if (messageBuffer == null || messageBuffer.hasRemaining()) { + throw new MessageNotReadException(); + } + + return SerializationUtils.deserialize(messageBuffer.array()); + } + + /** + * Resets message reader so that it can be used to read a new message. + */ + public void reset() { + isLengthRead = false; + lengthBuffer.clear(); + messageBuffer = null; + } +} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FTPMessageWriter.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FTPMessageWriter.java new file mode 100644 index 0000000..c13c33f --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FTPMessageWriter.java @@ -0,0 +1,53 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages.util; + +import org.apache.commons.lang3.SerializationUtils; +import org.jetbrains.annotations.NotNull; +import ru.spbau.bachelor2015.veselov.hw04.messages.FTPMessage; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions.LongMessageException; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; + +/** + * Message writer is a data writer which is responsible for writing a single ftp message to a specified channel. + */ +public class FTPMessageWriter implements DataWriter { + private final @NotNull WritableByteChannel channel; + + private final @NotNull ByteBuffer buffer; + + /** + * Creates a new message writer. + * + * @param channel a channel to which this message writer will be writing. + * @param message a message which this writer is responsible for. + * @throws LongMessageException if a given message is to long and can't be sent. + */ + public FTPMessageWriter(final @NotNull WritableByteChannel channel, final @NotNull FTPMessage message) + throws LongMessageException { + this.channel = channel; + + byte[] data = SerializationUtils.serialize(message); + + if (data.length > FTPMessage.MAXIMAL_MESSAGE_LENGTH) { + throw new LongMessageException(); + } + + buffer = ByteBuffer.allocate(Integer.BYTES + data.length); + buffer.putInt(data.length); + buffer.put(data); + buffer.flip(); + } + + /** + * Makes an attempt to write stored message to channel. + * + * @return true if message was fully written. + * @throws IOException if any IO exception occurs during writing process. + */ + public boolean write() throws IOException { + channel.write(buffer); + return !buffer.hasRemaining(); + } +} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FileEntry.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FileEntry.java new file mode 100644 index 0000000..8a8fed7 --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FileEntry.java @@ -0,0 +1,58 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages.util; + +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; +import java.nio.file.Path; + +/** + * An entry of a file in a filesystem. + */ +public class FileEntry implements Serializable { + private final @NotNull IndependentPath path; + + private final boolean isDirectory; + + /** + * Creates an entry. + * + * @param path a path to a file. + * @param isDirectory a flag which tells whether or not this entry represents a directory. + */ + public FileEntry(final @NotNull Path path, final boolean isDirectory) { + this.path = new IndependentPath(path); + this.isDirectory = isDirectory; + } + + /** + * Returns a path to a file. + */ + public @NotNull Path getPath() { + return path.toPath(); + } + + /** + * Returns true if this entry represents a directory, false otherwise. + */ + public boolean isDirectory() { + return isDirectory; + } + + /** + * Returns a string representation of file name. + */ + public @NotNull String getFileName() { + return getPath().getFileName().toString(); + } + + /** + * Returns a string representation of this file entry. + */ + public @NotNull String toString() { + if (isDirectory) { + return getPath() + " [directory]"; + } + + return getPath().toString(); + } +} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FileReceiver.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FileReceiver.java new file mode 100644 index 0000000..772da02 --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FileReceiver.java @@ -0,0 +1,96 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages.util; + +import org.jetbrains.annotations.NotNull; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions.FileWithNegativeLengthException; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.Path; + +/** + * File receiver is a data reader which is responsible for reading a content of a file from channel to a specified file. + */ +public class FileReceiver implements DataReader { + private static final int CHUNK_SIZE = 4096; + + private final @NotNull ReadableByteChannel channel; + + private final @NotNull FileChannel fileChannel; + + private boolean isLengthRead = false; + + private long bytesLeft; + + private final @NotNull ByteBuffer lengthBuffer = ByteBuffer.allocate(Long.BYTES); + + private final @NotNull ByteBuffer buffer = ByteBuffer.allocate(CHUNK_SIZE); + + /** + * Creates a new file receiver. + * + * @param channel a channel from which file receiver will be reading content of a file. + * @param path a path in a local file system where file receiver will be writing received data. + * @throws FileNotFoundException if opening of a file has failed. + */ + public FileReceiver(final @NotNull ReadableByteChannel channel, final @NotNull Path path) + throws FileNotFoundException { + this.channel = channel; + + fileChannel = new RandomAccessFile(path.toString(), "rw").getChannel(); + + } + + /** + * Makes an attempt to read a content of a file. + * + * @return the result of reading. + * @throws IOException if any IO exception occurs during reading process or if an invalid data was received. + */ + public @NotNull DataReader.ReadingResult read() throws IOException { + if (!isLengthRead) { + if (channel.read(lengthBuffer) == -1) { + return DataReader.ReadingResult.CLOSED; + } + + if (lengthBuffer.hasRemaining()) { + return DataReader.ReadingResult.NOT_READ; + } + + lengthBuffer.flip(); + bytesLeft = lengthBuffer.getLong(); + + if (bytesLeft < 0) { + fileChannel.close(); + throw new FileWithNegativeLengthException(); + } + + isLengthRead = true; + } + + while (bytesLeft > 0) { + int bytesRead = channel.read(buffer); + + if (bytesRead == -1) { + return DataReader.ReadingResult.CLOSED; + } + + bytesLeft -= bytesRead; + + if (buffer.hasRemaining() && bytesLeft > 0) { + return DataReader.ReadingResult.NOT_READ; + } + + buffer.flip(); + + fileChannel.write(buffer); + + buffer.clear(); + } + + return DataReader.ReadingResult.READ; + } +} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FileTransmitter.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FileTransmitter.java new file mode 100644 index 0000000..063273a --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/FileTransmitter.java @@ -0,0 +1,75 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages.util; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Path; + +/** + * File transmitter is a data writer which is responsible for writing a content of a specified file to a specified + * channel. + */ +public class FileTransmitter implements DataWriter { + private static final int CHUNK_SIZE = 4096; + + private final @NotNull WritableByteChannel channel; + + private final @NotNull ReadableByteChannel fileChannel; + + private final @NotNull ByteBuffer buffer = ByteBuffer.allocate(CHUNK_SIZE); + + /** + * Creates a new file transmitter. + * + * @param channel a channel to which this transmitter will be writing. + * @param path a path to a file which content transmitter will be writing. + * @throws IOException if any IO exception occurs during file opening. + */ + public FileTransmitter(final @NotNull WritableByteChannel channel, final @NotNull Path path) throws IOException { + this.channel = channel; + + RandomAccessFile file = new RandomAccessFile(path.toFile(), "r"); + fileChannel = file.getChannel(); + + buffer.putLong(file.length()); + buffer.flip(); + } + + /** + * Makes an attempt to write file content to channel. + * + * @return true if file content was fully written. + * @throws IOException if any IO exception occurs during writing process. + */ + public boolean write() throws IOException { + while (!isTransmitted()) { + channel.write(buffer); + + if (buffer.hasRemaining()) { + break; + } + + fillBuffer(); + } + + return isTransmitted(); + } + + private boolean isTransmitted() { + return !fileChannel.isOpen() && !buffer.hasRemaining(); + } + + private void fillBuffer() throws IOException { + buffer.clear(); + + if (fileChannel.read(buffer) == -1) { + fileChannel.close(); + } + + buffer.flip(); + } +} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/IndependentPath.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/IndependentPath.java new file mode 100644 index 0000000..4d9e724 --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/IndependentPath.java @@ -0,0 +1,39 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages.util; + +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * A file system independent path representation. Namely it's just a list of names. + */ +public class IndependentPath implements Serializable { + private final @NotNull List names; + + /** + * Creates a file system independent path from file system dependent java.nio.Path + * + * @param path a path which will be converted in independent path. + */ + public IndependentPath(final @NotNull Path path) { + names = StreamSupport.stream(Spliterators.spliteratorUnknownSize(path.iterator(), + Spliterator.ORDERED), + false) + .map(Path::toString) + .collect(Collectors.toList()); + } + + /** + * Returns an independent path converted to java.nio.Path with default file system. + */ + public @NotNull Path toPath() { + return Paths.get("", names.toArray(new String[0])); + } +} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/FileWithNegativeLengthException.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/FileWithNegativeLengthException.java new file mode 100644 index 0000000..269bd60 --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/FileWithNegativeLengthException.java @@ -0,0 +1,8 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions; + +import java.io.IOException; + +/** + * An exception which might be thrown by FileReceiver when a length of file appeared to be negative. + */ +public class FileWithNegativeLengthException extends IOException {} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/InvalidMessageException.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/InvalidMessageException.java new file mode 100644 index 0000000..2f4c1eb --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/InvalidMessageException.java @@ -0,0 +1,24 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +/** + * An exception which might be thrown by DataReaders when they have read invalid data. + */ +public class InvalidMessageException extends IOException { + /** + * An empty constructor. + */ + public InvalidMessageException() {} + + /** + * Constructor from cause. + * + * @param cause cause of the exception. + */ + public InvalidMessageException(final @NotNull Exception cause) { + super(cause); + } +} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/LongMessageException.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/LongMessageException.java new file mode 100644 index 0000000..19cdda3 --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/LongMessageException.java @@ -0,0 +1,7 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions; + +/** + * An exception which might be thrown by DataReaders and DataWriters when they encounter a message which length exceeds + * a predefined limit. + */ +public class LongMessageException extends InvalidMessageException {} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/MessageNotReadException.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/MessageNotReadException.java new file mode 100644 index 0000000..4961d53 --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/MessageNotReadException.java @@ -0,0 +1,7 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions; + +/** + * An exception which might be thrown by a FTPMessageReader if it's getMessage method invoked while the actual message + * hasn't been read. + */ +public class MessageNotReadException extends Exception {} diff --git a/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/MessageWithNonpositiveLengthException.java b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/MessageWithNonpositiveLengthException.java new file mode 100644 index 0000000..9f8a444 --- /dev/null +++ b/04-SimpleFTP/messages/src/main/java/ru/spbau/bachelor2015/veselov/hw04/messages/util/exceptions/MessageWithNonpositiveLengthException.java @@ -0,0 +1,6 @@ +package ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions; + +/** + * An exception which might be thrown by FTPMessageReader if length of a message appeared to be nonpositive. + */ +public class MessageWithNonpositiveLengthException extends InvalidMessageException {} diff --git a/04-SimpleFTP/server/build.gradle b/04-SimpleFTP/server/build.gradle new file mode 100644 index 0000000..3f3a372 --- /dev/null +++ b/04-SimpleFTP/server/build.gradle @@ -0,0 +1,29 @@ +group 'ru.spbau.bachelor2015.veselov.hw04' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile project(":messages") + + testCompile group: 'junit', name: 'junit', version: '4.11' + + compile group: 'org.jetbrains', name: 'annotations', version: '15.0' + compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.8.2' +} + +jar { + manifest { + attributes 'Main-Class': 'ru.spbau.bachelor2015.veselov.hw04.server.Main' + } + + from { + configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + } +} \ No newline at end of file diff --git a/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/FTPChannelObserver.java b/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/FTPChannelObserver.java new file mode 100644 index 0000000..1ec9164 --- /dev/null +++ b/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/FTPChannelObserver.java @@ -0,0 +1,155 @@ +package ru.spbau.bachelor2015.veselov.hw04.server; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import ru.spbau.bachelor2015.veselov.hw04.server.exceptions.NoDataWriterRegisteredException; +import ru.spbau.bachelor2015.veselov.hw04.server.exceptions.RegisteringSecondDataWriterException; +import ru.spbau.bachelor2015.veselov.hw04.messages.FTPMessage; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.DataWriter; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.FTPMessageReader; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.FTPMessageWriter; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.FileTransmitter; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions.InvalidMessageException; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions.LongMessageException; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions.MessageNotReadException; + +import java.io.IOException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.nio.file.Path; + +/** + * Channel observer handles all operations on a particular socket channel and manages this socket channel lifecycle. + */ +public class FTPChannelObserver { + private final static @NotNull Logger logger = LogManager.getLogger(FTPChannelObserver.class.getCanonicalName()); + + private final @NotNull SocketChannel channel; + + private final @NotNull SelectionKey selectionKey; + + private final @NotNull Server server; + + private final @NotNull FTPMessageReader reader; + + private @Nullable DataWriter writer; + + /** + * Creates a new observer. + * + * @param channel a channel for which a new observer will be created. + * @param server a server which established a connection this channel represents. + * @param selector a selector of a server. + * @throws IOException if any IO exception occurs during observer creation. + */ + public FTPChannelObserver(final @NotNull SocketChannel channel, + final @NotNull Server server, + final @NotNull Selector selector) throws IOException { + logger.info("A new FTPChannelObserver ({}) has been created", this); + + this.channel = channel; + this.server = server; + + channel.configureBlocking(false); + + selectionKey = channel.register(selector, SelectionKey.OP_READ, this); + + reader = new FTPMessageReader(channel); + } + + /** + * Registers a message writer for stored channel. + * + * @param message a message which this writer will be writing. + * @throws RegisteringSecondDataWriterException if there is already a registered writer. + * @throws LongMessageException if a message is too long. + */ + public void registerMessageWriter(final @NotNull FTPMessage message) + throws RegisteringSecondDataWriterException, LongMessageException { + if (writer != null) { + throw new RegisteringSecondDataWriterException(); + } + + logger.info("A new FTPMessageWriter has been registered in {}", this); + + selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE); + writer = new FTPMessageWriter(channel, message); + } + + /** + * Registers a file transmitter for stored channel. + * + * @param path a path to a file which content will be transmitted. + * @throws RegisteringSecondDataWriterException if there is already a registered writer. + * @throws IOException if any IO exception occurs during file opening. + */ + public void registerFileTransmitter(final @NotNull Path path) + throws RegisteringSecondDataWriterException, IOException { + if (writer != null) { + throw new RegisteringSecondDataWriterException(); + } + + logger.info("A new FileTransmitter has been registered in {}", this); + + selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE); + writer = new FileTransmitter(channel, path); + } + + /** + * Makes an attempt to read a message from stored channel. + * + * @throws IOException if any IO exception occurs during reading process. + */ + public void read() throws IOException { + try { + switch (reader.read()) { + case NOT_READ: + return; + + case READ: + FTPMessage message; + + try { + message = reader.getMessage(); + } catch (MessageNotReadException e) { + throw new RuntimeException(e); + } + + reader.reset(); + + server.handleMessage(channel, message); + break; + + case CLOSED: + channel.close(); + break; + } + } catch (InvalidMessageException e) { + logger.info("{} has read an invalid message", this); + + channel.close(); + } + } + + /** + * Makes an attempt to write something with registered writer. + * + * @throws IOException if any IO exception occurs during writing process. + * @throws NoDataWriterRegisteredException if there is no data writer registered. + */ + public void write() throws IOException, NoDataWriterRegisteredException { + if (writer == null) { + throw new NoDataWriterRegisteredException(); + } + + if (writer.write()) { + logger.info("{} has written data to a channel", this); + + writer = null; + selectionKey.interestOps(selectionKey.interestOps() & ~SelectionKey.OP_WRITE); + } + } +} diff --git a/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/Main.java b/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/Main.java new file mode 100644 index 0000000..8000900 --- /dev/null +++ b/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/Main.java @@ -0,0 +1,53 @@ +package ru.spbau.bachelor2015.veselov.hw04.server; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Paths; + +/** + * Entry point of a server application. This class allows to start and stop server through a console. + */ +public class Main { + /** + * Entry method. + * + * @param args two arguments are expected. First is a path to a folder which will be tracked by server, second is a + * port which the server will be bound to. + * @throws IOException any IO exception which may occur during reading of commands. + * @throws InterruptedException if main thread was interrupted. + */ + public static void main(String[] args) throws IOException, InterruptedException { + if (args.length != 2) { + System.out.println("Two arguments expected: "); + return; + } + + Server server = new Server(Paths.get(args[0]), Integer.parseInt(args[1])); + + boolean shouldRun = true; + + try (InputStreamReader inputStreamReader = new InputStreamReader(System.in); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + while (shouldRun) { + String command = bufferedReader.readLine(); + switch (command) { + case "start": + server.start(); + break; + + case "stop": + server.stop(); + break; + + case "exit": + shouldRun = false; + break; + + default: + System.out.println("Unknown command: " + command); + } + } + } + } +} diff --git a/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/Server.java b/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/Server.java new file mode 100644 index 0000000..3c37314 --- /dev/null +++ b/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/Server.java @@ -0,0 +1,214 @@ +package ru.spbau.bachelor2015.veselov.hw04.server; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import ru.spbau.bachelor2015.veselov.hw04.messages.FTPGetMessage; +import ru.spbau.bachelor2015.veselov.hw04.messages.FTPListAnswerMessage; +import ru.spbau.bachelor2015.veselov.hw04.messages.FTPListMessage; +import ru.spbau.bachelor2015.veselov.hw04.messages.FTPMessage; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.FileEntry; +import ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions.InvalidMessageException; +import ru.spbau.bachelor2015.veselov.hw04.server.exceptions.InvalidPathException; +import ru.spbau.bachelor2015.veselov.hw04.server.exceptions.NoDataWriterRegisteredException; +import ru.spbau.bachelor2015.veselov.hw04.server.exceptions.NoSuchMessageException; +import ru.spbau.bachelor2015.veselov.hw04.server.exceptions.RegisteringSecondDataWriterException; + +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * Server class can accept a connections and deal with them by answering on requests. + * + * TODO: travis + */ +public class Server { + private final static @NotNull Logger logger = LogManager.getLogger(Server.class.getCanonicalName()); + + private volatile boolean shouldRun; + + private final int port; + + private final @NotNull Path trackedFolder; + + private final @NotNull Thread serverThread; + + private volatile @Nullable Selector selector; + + /** + * Creates a new server. + * + * @param trackedFolder a folder which is tracked by this new server. Server can give content only of this folder. + * @param port a port which this server will be bound to. + */ + public Server(final @NotNull Path trackedFolder, final int port) { + logger.info("New Server has been created"); + + this.trackedFolder = trackedFolder; + this.port = port; + + this.serverThread = + new Thread( + () -> { + try (Selector selector = Selector.open(); + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) { + serverSocketChannel.socket().bind(new InetSocketAddress(this.port)); + serverSocketChannel.configureBlocking(false); + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + + this.selector = selector; + + logger.info("Server has been started"); + + while (shouldRun) { + selector.select(); + + for (SelectionKey key : selector.selectedKeys()) { + try { + if (key.isAcceptable()) { + acceptNewConnection(serverSocketChannel); + } + + if (key.isReadable()) { + ((FTPChannelObserver) key.attachment()).read(); + + if (!key.channel().isOpen()) { + continue; + } + } + + if (key.isWritable()) { + try { + ((FTPChannelObserver) key.attachment()).write(); + } catch (NoDataWriterRegisteredException e) { + throw new RuntimeException(e); + } + } + } catch (IOException e) { + logger.error( + "IOException has occurred during interaction of Server " + + "with a connection.\n{}", e); + + key.channel().close(); + } + } + + selector.selectedKeys().clear(); + } + + for (SelectionKey key : selector.keys()) { + key.channel().close(); + } + + logger.info("Server has been stopped"); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + selector = null; + shouldRun = false; + } + } + ); + } + + /** + * Starts this server in a new thread. + */ + public void start() { + shouldRun = true; + serverThread.start(); + } + + /** + * Stops this server thread. + * + * @throws InterruptedException if this thread was interrupted while waiting for server thread to join. + */ + public void stop() throws InterruptedException { + if (!serverThread.isAlive()) { + return; + } + + shouldRun = false; + selector.wakeup(); + serverThread.join(); + } + + private void acceptNewConnection(final @NotNull ServerSocketChannel serverSocketChannel) throws IOException { + SocketChannel socketChannel = serverSocketChannel.accept(); + if (socketChannel == null) { + return; + } + + logger.info("Server has accepted a new connection"); + new FTPChannelObserver(socketChannel, this, selector); + } + + void handleMessage(final @NotNull SocketChannel channel, final @NotNull FTPMessage message) throws IOException { + logger.info("Server has received a new message"); + + // TODO: add double dispatch + if (message instanceof FTPListMessage) { + handleMessage(channel.keyFor(selector), (FTPListMessage) message); + } else if (message instanceof FTPGetMessage) { + handleMessage(channel.keyFor(selector), (FTPGetMessage) message); + } else { + throw new NoSuchMessageException(); + } + } + + private void handleMessage(final @NotNull SelectionKey key, final @NotNull FTPListMessage message) + throws IOException { + Path path = realPath(message.getPath()); + + File[] files = path.toFile().listFiles(); + + List entries = new ArrayList<>(); + + if (files != null) { + for (File file : files) { + entries.add(new FileEntry(trackedFolder.relativize(file.toPath()), + file.isDirectory())); + } + } + + try { + ((FTPChannelObserver) key.attachment()).registerMessageWriter(new FTPListAnswerMessage(entries)); + } catch (RegisteringSecondDataWriterException e) { + throw new InvalidMessageException(e); + } + } + + private void handleMessage(final @NotNull SelectionKey key, final @NotNull FTPGetMessage message) + throws IOException { + Path path = realPath(message.getPath()); + + try { + ((FTPChannelObserver) key.attachment()).registerFileTransmitter(path); + } catch (RegisteringSecondDataWriterException e) { + throw new InvalidMessageException(e); + } + } + + private @NotNull Path realPath(final @NotNull Path path) throws InvalidPathException { + if (path.isAbsolute()) { + throw new InvalidPathException(); + } + + Path real = trackedFolder.resolve(path).normalize(); + if (!trackedFolder.equals(real) && trackedFolder.startsWith(real)) { + throw new InvalidPathException(); + } + + return real; + } +} diff --git a/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/exceptions/InvalidPathException.java b/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/exceptions/InvalidPathException.java new file mode 100644 index 0000000..89d1419 --- /dev/null +++ b/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/exceptions/InvalidPathException.java @@ -0,0 +1,8 @@ +package ru.spbau.bachelor2015.veselov.hw04.server.exceptions; + +import ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions.InvalidMessageException; + +/** + * An exception which might be thrown by server if path in incoming request is invalid. + */ +public class InvalidPathException extends InvalidMessageException {} diff --git a/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/exceptions/NoDataWriterRegisteredException.java b/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/exceptions/NoDataWriterRegisteredException.java new file mode 100644 index 0000000..793a07b --- /dev/null +++ b/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/exceptions/NoDataWriterRegisteredException.java @@ -0,0 +1,7 @@ +package ru.spbau.bachelor2015.veselov.hw04.server.exceptions; + +/** + * An exception which might be thrown by FTPChannelObserver if it's write method is invoked while no DataWriter was + * registered. + */ +public class NoDataWriterRegisteredException extends Exception {} diff --git a/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/exceptions/NoSuchMessageException.java b/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/exceptions/NoSuchMessageException.java new file mode 100644 index 0000000..d33c28f --- /dev/null +++ b/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/exceptions/NoSuchMessageException.java @@ -0,0 +1,8 @@ +package ru.spbau.bachelor2015.veselov.hw04.server.exceptions; + +import ru.spbau.bachelor2015.veselov.hw04.messages.util.exceptions.InvalidMessageException; + +/** + * An exception which might be thrown by a Server if it can't recognize an incoming message. + */ +public class NoSuchMessageException extends InvalidMessageException {} diff --git a/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/exceptions/RegisteringSecondDataWriterException.java b/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/exceptions/RegisteringSecondDataWriterException.java new file mode 100644 index 0000000..7113abe --- /dev/null +++ b/04-SimpleFTP/server/src/main/java/ru/spbau/bachelor2015/veselov/hw04/server/exceptions/RegisteringSecondDataWriterException.java @@ -0,0 +1,7 @@ +package ru.spbau.bachelor2015.veselov.hw04.server.exceptions; + +/** + * An exception which might be thrown by a FTPChannelObserver if there is an attempt to register DataWriter while + * another one already registered. + */ +public class RegisteringSecondDataWriterException extends Exception {} diff --git a/04-SimpleFTP/server/src/main/resources/log4j2.xml b/04-SimpleFTP/server/src/main/resources/log4j2.xml new file mode 100644 index 0000000..6ad75d3 --- /dev/null +++ b/04-SimpleFTP/server/src/main/resources/log4j2.xml @@ -0,0 +1,23 @@ + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + diff --git a/04-SimpleFTP/settings.gradle b/04-SimpleFTP/settings.gradle new file mode 100644 index 0000000..ec371c9 --- /dev/null +++ b/04-SimpleFTP/settings.gradle @@ -0,0 +1,9 @@ +rootProject.name = '04-SimpleFTP' +include 'messages' +include 'server' +include 'console-client' +include 'integration-tests' +include 'graphic-client' +include 'client' +include 'client' +