diff --git a/HomeWork_1.md b/HomeWork_1.md
new file mode 100644
index 0000000..ac886aa
--- /dev/null
+++ b/HomeWork_1.md
@@ -0,0 +1,117 @@
+# Домашнее задание к занятию «1.1. HTTP и современный Web»
+
+В качестве результата пришлите ссылки на ваши GitHub-проект в личном кабинете студента на сайте [netology.ru](https://netology.ru).
+
+**Важно**: ознакомьтесь со ссылками, представленными на главной странице [репозитория с домашними заданиями](../README.md).
+
+**Важно**: если у вас что-то не получилось, то оформляйте Issue [по установленным правилам](../report-requirements.md).
+
+**Важно**: все задачи нужно делать в **одном** репозитории.
+
+## Как сдавать задачи
+
+1. Создайте на вашем компьютере Maven-проект
+1. Инициализируйте в нём пустой Git-репозиторий
+1. Добавьте в него готовый файл [.gitignore](../.gitignore)
+1. Добавьте в этот же каталог остальные необходимые файлы
+1. Сделайте необходимые коммиты
+1. Создайте публичный репозиторий на GitHub и свяжите свой локальный репозиторий с удалённым
+1. Сделайте пуш (удостоверьтесь, что ваш код появился на GitHub)
+1. Ссылку на ваш проект отправьте в личном кабинете на сайте [netology.ru](https://netology.ru)
+
+## Refactoring & MultiThreading
+
+### Легенда
+
+Достаточно часто после того, как прототип проверен (мы про то, что было реализовано на лекции), возникает задача привести это в более-менее нормальный вид: выделить классы, методы, обеспечить должную функциональность.
+
+### Задача
+
+Необходимо отрефакторить код, рассмотренный на лекции, и применить все те знания, которые у вас есть:
+1. Выделить класс `Server` с методами для
+ - запуска
+ - обработки конкретного подключения
+1. Реализовать обработку подключений с помощью `ThreadPool`'а (выделите фиксированный на 64 потока и каждое подключение обрабатывайте в потоке из пула)
+
+Поскольку вы - главный архитектор и проектировщик данного небольшого класса, то все архитектурные решения принимать вам, но будьте готовы к критике со стороны проверяющих.
+
+### Результат
+
+В качестве результата пришлите ссылку на ваш проект на GitHub в личном кабинете студента на сайте [netology.ru](https://netology.ru).
+
+## Handlers*
+
+**Важно**: это необязательная задача, её выполнение не влияет на получение зачёта.
+
+### Легенда
+
+Сервер, который вы написали в предыдущей задаче, - это, конечно, здорово, но пока он не расширяем и его нельзя переиспользовать, т.к. код обработки зашит прямо внутрь сервера.
+
+Давайте попробуем его сделать немного полезнее.
+
+Что хотим сделать? Мы хотим сделать так, чтобы в сервер можно было добавлять обработчики на определённые шаблоны путей.
+
+Что это значит? Мы хотим, чтобы можно было сделать вот так:
+
+```java
+public class Main {
+ public static void main(String[] args){
+ final var server = new Server();
+ // код инициализации сервера (из вашего предыдущего ДЗ)
+
+ // добавление handler'ов (обработчиков)
+ server.addHandler("GET", "/messages", new Handler() {
+ public void handle(Request request, BufferedOutputStream responseStream) {
+ // TODO: handlers code
+ }
+ });
+ server.addHandler("POST", "/messages", new Handler() {
+ public void handle(Request request, BufferedOutputStream responseStream) {
+ // TODO: handlers code
+ }
+ });
+
+ server.listen(9999);
+ }
+}
+```
+
+В итоге на запрос типа GET на путь "/messages" будет вызван первый обработчик, на запрос типа POST и путь "/messages" будет вызван второй.
+
+Как вы видите - `Handler` из себя представляет функциональный интерфейс всего с одним методом (может быть заменён на lambda).
+
+`Request` - это класс, который проектируете вы сами, для нас важно, чтобы он содержал:
+1. Метод запроса (потому что на разные методы можно назначить один и тот же Handler)
+1. Заголовки запроса
+1. Тело запроса (если есть)
+
+`BufferedOutputStream` берётся просто путём заворачивания `OutputStream`'а `socket`'а: `new BufferedOutputStream(socket.getOutputStream())`.
+
+### Задача
+
+Реализуйте требования, указанные в легенде.
+
+
+Подсказки по реализации
+
+1. Вы принимаете запрос, парсите его целиком (как мы сделали на лекции) и собираете объект типа `Request`
+1. На основании данных из `Request` вы выбираете handler (он может быть только один), который и будет обрабатывать запрос
+1. Все handler'ы должны храниться в полях `Server`'а
+1. Самый простой способ хранить handler'ы - это использовать в качестве ключей метод и путь (можно как сделать `Map` внутри `Map`, так и отдельные `Map`'ы на каждый метод)
+1. Поиск хендлера заключается в том, что вы выбираете по нужному методу все зарегистрированные handler'ы, а затем перебираете по пути (используйте пока точное соответствие: считайте, что у вас все запросы без Query String)
+1. Найдя нужный handler - достаточно вызвать его метод `handle`, передав туда `Request` и `BufferedOutputStream`
+1. Поскольку ваш сервер многопоточный - думайте о том, как вы будете безопасно хранить handler'ы
+1. В качестве Body достаточно передавать `InputStream` (напоминаем, Body начинается после `\r\n\r\n`
+
+Итого: фактически вы решаете задачу поиска элемента в "коллекции" с вызовом его метода.
+
+
+### Результат
+
+Реализуйте новую функциональность в ветке `feature/handlers` вашего репозитория из ДЗ 1 и откройте Pull Request.
+
+Поскольку вы - главный архитектор и проектировщик данного решения (уже более функционального), то все архитектурные решения принимать вам, но будьте готовы к критике со стороны проверяющих.
+
+В качестве результата пришлите ссылку на ваш Pull Request на GitHub в личном кабинете студента на сайте [netology.ru](https://netology.ru).
+
+После того, как ДЗ будет принято, сделайте `merge` для Pull Request'а.
\ No newline at end of file
diff --git a/README.md b/README.md
index ac886aa..8765516 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,11 @@
-# Домашнее задание к занятию «1.1. HTTP и современный Web»
+# Домашнее задание к занятию «1.2. Формы и форматы передачи данных»
-В качестве результата пришлите ссылки на ваши GitHub-проект в личном кабинете студента на сайте [netology.ru](https://netology.ru).
+В качестве результата пришлите ссылки на ваши GitHub-проекты в личном кабинете студента на сайте [netology.ru](https://netology.ru).
**Важно**: ознакомьтесь со ссылками, представленными на главной странице [репозитория с домашними заданиями](../README.md).
**Важно**: если у вас что-то не получилось, то оформляйте Issue [по установленным правилам](../report-requirements.md).
-**Важно**: все задачи нужно делать в **одном** репозитории.
-
## Как сдавать задачи
1. Создайте на вашем компьютере Maven-проект
@@ -19,96 +17,81 @@
1. Сделайте пуш (удостоверьтесь, что ваш код появился на GitHub)
1. Ссылку на ваш проект отправьте в личном кабинете на сайте [netology.ru](https://netology.ru)
-## Refactoring & MultiThreading
+## query
### Легенда
-Достаточно часто после того, как прототип проверен (мы про то, что было реализовано на лекции), возникает задача привести это в более-менее нормальный вид: выделить классы, методы, обеспечить должную функциональность.
+В рамках изучения Java Core и изучения работы протокола HTTP вы использовали библиотеку HttpClient из состава [Apache HttpComponents](https://hc.apache.org).
-### Задача
+В состав данной библиотеки входит утилитный класс [URLEncodedUtils](http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/client/utils/URLEncodedUtils.html), который и позволяет "парсить" Query String, извлекая параметры.
-Необходимо отрефакторить код, рассмотренный на лекции, и применить все те знания, которые у вас есть:
-1. Выделить класс `Server` с методами для
- - запуска
- - обработки конкретного подключения
-1. Реализовать обработку подключений с помощью `ThreadPool`'а (выделите фиксированный на 64 потока и каждое подключение обрабатывайте в потоке из пула)
+Необходимо добавить в ваш сервер (из предыдущего ДЗ) функциональность обработки параметров запроса так, чтобы можно было из объекта типа `Request` отдельно получать и путь запроса, и параметры из Query String.
-Поскольку вы - главный архитектор и проектировщик данного небольшого класса, то все архитектурные решения принимать вам, но будьте готовы к критике со стороны проверяющих.
+Например, это можно сделать в виде метода `getQueryParam(String name)` и `getQueryParams()`. Подумайте, что они должны возвращать, исходя из документации на утилитный класс.
+
+### Задача
+
+1. Подключите к своему проекту HttpClient
+1. Реализуйте функциональность по обработке параметров из Query
+1. При необходимости, доработайте функциональность поиска handler'а так, чтобы учитывался только путь без Query: т.е. handler, зарегистрированный на "/messages", обрабатывал и запросы "/messages?last=10"
### Результат
-В качестве результата пришлите ссылку на ваш проект на GitHub в личном кабинете студента на сайте [netology.ru](https://netology.ru).
+Реализуйте новую функциональность в ветке `feature/query` вашего репозитория из ДЗ 1 и откройте Pull Request.
+
+Поскольку вы - главный архитектор и проектировщик данного решения (уже более функционального), то все архитектурные решения принимать вам, но будьте готовы к критике со стороны проверяющих.
+
+В качестве результата пришлите ссылку на ваш Pull Request на GitHub в личном кабинете студента на сайте [netology.ru](https://netology.ru).
+
+После того, как ДЗ будет принято, сделайте `merge` для Pull Request'а.
-## Handlers*
+## x-www-form-urlencoded* (задача со звёздочкой)
-**Важно**: это необязательная задача, её выполнение не влияет на получение зачёта.
+**Важно**: выполнение данного ДЗ не влияет на получение зачёта по ДЗ.
### Легенда
-Сервер, который вы написали в предыдущей задаче, - это, конечно, здорово, но пока он не расширяем и его нельзя переиспользовать, т.к. код обработки зашит прямо внутрь сервера.
+Необходимо добавить в ваш сервер (из предыдущего ДЗ) функциональность обработки тела, оформленного в виде `x-www-form-url-encoded`, запроса так, чтобы можно было из объекта типа `Request` отдельно параметры, переданные в теле запроса.
-Давайте попробуем его сделать немного полезнее.
+Например, это можно сделать в виде метода `getPostParam(String name)` и `getPostParams()`. Подумайте, что они должны возвращать, исходя из того, что в передаваемой вам форме может быть два параметра с одинаковым именем.
-Что хотим сделать? Мы хотим сделать так, чтобы в сервер можно было добавлять обработчики на определённые шаблоны путей.
+### Задача
-Что это значит? Мы хотим, чтобы можно было сделать вот так:
+Реализуйте функциональность по обработке тела запроса, если он представлен в виде `x-www-form-urlencoded`.
-```java
-public class Main {
- public static void main(String[] args){
- final var server = new Server();
- // код инициализации сервера (из вашего предыдущего ДЗ)
+### Результат
- // добавление handler'ов (обработчиков)
- server.addHandler("GET", "/messages", new Handler() {
- public void handle(Request request, BufferedOutputStream responseStream) {
- // TODO: handlers code
- }
- });
- server.addHandler("POST", "/messages", new Handler() {
- public void handle(Request request, BufferedOutputStream responseStream) {
- // TODO: handlers code
- }
- });
+Реализуйте новую функциональность в ветке `feature/form` вашего репозитория из предыдущего ДЗ и откройте Pull Request.
- server.listen(9999);
- }
-}
-```
+Поскольку вы - главный архитектор и проектировщик данного решения (уже более функционального), то все архитектурные решения принимать вам, но будьте готовы к критике со стороны проверяющих.
-В итоге на запрос типа GET на путь "/messages" будет вызван первый обработчик, на запрос типа POST и путь "/messages" будет вызван второй.
+В качестве результата пришлите ссылку на ваш Pull Request на GitHub в личном кабинете студента на сайте [netology.ru](https://netology.ru).
-Как вы видите - `Handler` из себя представляет функциональный интерфейс всего с одним методом (может быть заменён на lambda).
+После того, как ДЗ будет принято, сделайте `merge` для Pull Request'а.
-`Request` - это класс, который проектируете вы сами, для нас важно, чтобы он содержал:
-1. Метод запроса (потому что на разные методы можно назначить один и тот же Handler)
-1. Заголовки запроса
-1. Тело запроса (если есть)
+## multipart/form-data* (задача со звёздочкой)
-`BufferedOutputStream` берётся просто путём заворачивания `OutputStream`'а `socket`'а: `new BufferedOutputStream(socket.getOutputStream())`.
+**Важно**: выполнение данного ДЗ не влияет на получение зачёта по ДЗ.
-### Задача
+### Легенда
+
+Почему бы не реализовать "полную" функциональность и не добавить поддержку multipart-запросов? Используйте библиотеку [FileUpload](http://commons.apache.org/proper/commons-fileupload/), чтобы добавить подобную функциональность.
-Реализуйте требования, указанные в легенде.
+Например, это можно сделать в виде метода `getPart(String name)` и `getParts()`. Подумайте, что они должны возвращать, исходя из того, что:
+1. В передаваемой вам форме может быть два параметра с одинаковым именем
+1. Каждый `Part` может быть как файлом, так и обычным полем
-
-Подсказки по реализации
+Всё это значит, что нужно как-то уметь отличать обычные поля от файлов.
-1. Вы принимаете запрос, парсите его целиком (как мы сделали на лекции) и собираете объект типа `Request`
-1. На основании данных из `Request` вы выбираете handler (он может быть только один), который и будет обрабатывать запрос
-1. Все handler'ы должны храниться в полях `Server`'а
-1. Самый простой способ хранить handler'ы - это использовать в качестве ключей метод и путь (можно как сделать `Map` внутри `Map`, так и отдельные `Map`'ы на каждый метод)
-1. Поиск хендлера заключается в том, что вы выбираете по нужному методу все зарегистрированные handler'ы, а затем перебираете по пути (используйте пока точное соответствие: считайте, что у вас все запросы без Query String)
-1. Найдя нужный handler - достаточно вызвать его метод `handle`, передав туда `Request` и `BufferedOutputStream`
-1. Поскольку ваш сервер многопоточный - думайте о том, как вы будете безопасно хранить handler'ы
-1. В качестве Body достаточно передавать `InputStream` (напоминаем, Body начинается после `\r\n\r\n`
+### Задача
-Итого: фактически вы решаете задачу поиска элемента в "коллекции" с вызовом его метода.
-
+Реализуйте функциональность по обработке тела запроса, если от представлен в виде `multipart/form-data`.
+
+Поскольку вы - главный архитектор и проектировщик данного небольшого класса, то все архитектурные решения принимать вам, но будьте готовы к критике со стороны проверяющих.
### Результат
-Реализуйте новую функциональность в ветке `feature/handlers` вашего репозитория из ДЗ 1 и откройте Pull Request.
+Реализуйте новую функциональность в ветке `feature/multipart` вашего репозитория из предыдущего ДЗ и откройте Pull Request.
Поскольку вы - главный архитектор и проектировщик данного решения (уже более функционального), то все архитектурные решения принимать вам, но будьте готовы к критике со стороны проверяющих.
diff --git a/pom.xml b/pom.xml
index 186b774..79576b1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,4 +13,13 @@
11
+
+
+
+ org.apache.httpcomponents
+ httpclient
+ 4.5.13
+
+
+
\ No newline at end of file
diff --git a/src/main/java/Main.java b/src/main/java/Main.java
index c47ddde..2fe1f2c 100644
--- a/src/main/java/Main.java
+++ b/src/main/java/Main.java
@@ -14,7 +14,7 @@ public static void main(String[] args) {
// добавление handler'ов (обработчиков)
server.addHandler("GET", "/messages", (request, responseStream) -> {
try {
- server.responseWithoutContent(responseStream, "404", "Not Found");
+ server.responseWithoutContent(responseStream, "200", "OK");
} catch (IOException e) {
throw new RuntimeException(e);
}
@@ -24,6 +24,7 @@ public static void main(String[] args) {
server.addHandler("GET", "/", ((request, outputStream) -> server.defaultHandler(outputStream, "index.html")));
+
// Start
server.start();
}
diff --git a/src/main/java/Request.java b/src/main/java/Request.java
index 1a6281d..11b0f24 100644
--- a/src/main/java/Request.java
+++ b/src/main/java/Request.java
@@ -1,15 +1,43 @@
+import org.apache.http.NameValuePair;
+import org.apache.http.client.utils.URLEncodedUtils;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+
/**
+ * Represents HTTP request received from a server.
+ *
* @author Stanislav Rakitov
+ * @version 1.2.1
*/
public class Request {
private final String method;
private final String path;
+ private final List headers;
+// private byte[] body;
+
+ public static final String GET = "GET";
+ public static final String POST = "POST";
+ private List params;
public Request(String requestMethod, String requestPath) {
this.method = requestMethod;
this.path = requestPath;
+ headers = null;
+ }
+
+ public Request(String method, String path, List headers, List params) {
+ this.method = method;
+ this.path = path;
+ this.headers = headers;
+ this.params = params;
}
public String getMethod() {
@@ -20,4 +48,90 @@ public String getPath() {
return path;
}
+ static Request createRequest(BufferedInputStream in) throws IOException, URISyntaxException {
+ final List allowedMethods = List.of(GET, POST);
+
+ final var limit = 4096;
+ in.mark(limit);
+ final var buffer = new byte[limit];
+ final var read = in.read(buffer);
+
+ // ищем request line
+ final var requestLineDelimiter = new byte[]{'\r', '\n'};
+ final var requestLineEnd = indexOf(buffer, requestLineDelimiter, 0, read);
+ if (requestLineEnd == -1) {
+ return null;
+ }
+
+ // читаем request line
+ final var requestLine = new String(Arrays.copyOf(buffer, requestLineEnd)).split(" ");
+ if (requestLine.length != 3) {
+ return null;
+ }
+
+ final var method = requestLine[0];
+ if (!allowedMethods.contains(method)) {
+ return null;
+ }
+
+ final var path = requestLine[1];
+
+ // ищем заголовки
+ final var headersDelimiter = new byte[]{'\r', '\n', '\r', '\n'};
+ final var headersStart = requestLineEnd + requestLineDelimiter.length;
+ final var headersEnd = indexOf(buffer, headersDelimiter, headersStart, read);
+ if (headersEnd == -1) {
+ return null;
+ }
+
+ // отматываем на начало буфера
+ in.reset();
+ // пропускаем requestLine
+ in.skip(headersStart);
+
+ final var headersBytes = in.readNBytes(headersEnd - headersStart);
+ List headers = Arrays.asList(new String(headersBytes).split("\r\n"));
+
+ List params = URLEncodedUtils.parse(new URI(path), StandardCharsets.UTF_8);
+
+ return new Request(method, path, headers, params);
+ }
+
+ // from Google guava with modifications
+ private static int indexOf(byte[] array, byte[] target, int start, int max) {
+ outer:
+ for (int i = start; i < max - target.length + 1; i++) {
+ for (int j = 0; j < target.length; j++) {
+ if (array[i + j] != target[j]) {
+ continue outer;
+ }
+ }
+ return i;
+ }
+ return -1;
+ }
+
+ public NameValuePair getQueryParam(String name) {
+ return getQueryParams().stream()
+ .filter(param -> param.getName().equalsIgnoreCase(name))
+ .findFirst().orElse(new NameValuePair() {
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getValue() {
+ return "";
+ }
+ });
+ }
+
+ public List getQueryParams() {
+ return params;
+ }
+
+ public List getHeaders() {
+ return headers;
+ }
}
diff --git a/src/main/java/Server.java b/src/main/java/Server.java
index 9d59230..06b872d 100644
--- a/src/main/java/Server.java
+++ b/src/main/java/Server.java
@@ -1,9 +1,9 @@
+import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
-import java.io.BufferedReader;
import java.io.IOException;
-import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
+import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
@@ -14,6 +14,12 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+/**
+ * simple http Server
+ *
+ * @author Stanislav Rakitov
+ * @version 1.2.1
+ */
public class Server {
private final int SERVER_SOCKET;
@@ -44,56 +50,68 @@ void start() {
}
private void proceedConnection(Socket socket) {
- try (final var in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- final var out = new BufferedOutputStream(socket.getOutputStream())) {
-
- // read only request line for simplicity
- // must be in form GET /path HTTP/1.1
- final var requestLine = in.readLine();
- final var parts = requestLine.split(" ");
-
- if (parts.length != 3) {
- // just close socket
- socket.close();
- return;
- }
-
- String method = parts[0];
- final var path = parts[1];
- Request request = createRequest(method, path);
+ try (final var in = new BufferedInputStream(socket.getInputStream());
+ final var out = new BufferedOutputStream(socket.getOutputStream())
+ ) {
+ Request request = Request.createRequest(in);
// Check for bad requests and drop connection
if (request == null || !handlers.containsKey(request.getMethod())) {
responseWithoutContent(out, "400", "Bad Request");
return;
+ } else {
+ // Print out debug info for request
+ printRequestDebug(request);
}
+
// Get PATH, HANDLER Map
Map handlerMap = handlers.get(request.getMethod());
- String requestPath = request.getPath();
+ String requestPath = request.getPath().split("\\?")[0];
if (handlerMap.containsKey(requestPath)) {
Handler handler = handlerMap.get(requestPath);
handler.handle(request, out);
} else { // Defaults
// Resource not found
- if (!validPaths.contains(request.getPath())) {
+ if (!validPaths.contains(requestPath)) {
responseWithoutContent(out, "404", "Not Found");
} else {
- defaultHandler(out, path);
+ defaultHandler(out, requestPath);
}
}
- } catch (IOException e) {
+ } catch (IOException | URISyntaxException e) {
throw new RuntimeException(e);
}
}
+ /**
+ * Printout debug information for Request object
+ *
+ * @param request Request to printout
+ */
+ private void printRequestDebug(Request request) {
+ System.out.println("Request debug information: ");
+ System.out.println("METHOD: " + request.getMethod());
+ System.out.println("PATH: " + request.getPath());
+ System.out.println("HEADERS: " + request.getHeaders());
+ System.out.println("Query Params:");
+ for (var para : request.getQueryParams()) {
+ System.out.println(para.getName() + " = " + para.getValue());
+ }
+
+ System.out.println("Test for dumb param name:");
+ System.out.println(request.getQueryParam("YetAnotherDumb").getName());
+ System.out.println("Test for dumb param name-value:");
+ System.out.println(request.getQueryParam("testDebugInfo").getValue());
+ }
+
void defaultHandler(BufferedOutputStream out, String path) throws IOException {
final var filePath = Path.of(".", "public", path);
final var mimeType = Files.probeContentType(filePath);
// special case for classic
- if (path.equals("/classic.html")) {
+ if (path.startsWith("/classic.html")) {
final var template = Files.readString(filePath);
final var content = template.replace(
"{time}",
@@ -123,16 +141,6 @@ void defaultHandler(BufferedOutputStream out, String path) throws IOException {
out.flush();
}
- private Request createRequest(String method, String path) {
- // TODO: More checks for bad fields
- if (method != null && !method.isBlank()) {
- return new Request(method, path);
- } else {
- return null;
- }
-
- }
-
void addHandler(String method, String path, Handler handler) {
if (!handlers.containsKey(method)) {
handlers.put(method, new HashMap<>());
@@ -149,6 +157,7 @@ void responseWithoutContent(BufferedOutputStream out, String responseCode, Strin
).getBytes());
out.flush();
}
+
}