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'
+