diff --git a/src/data/Endpoint.java b/src/data/Endpoint.java new file mode 100644 index 0000000..7bb54fd --- /dev/null +++ b/src/data/Endpoint.java @@ -0,0 +1,38 @@ +package data; + +public enum Endpoint { + + BASE, + + // Задачи + GET_TASK, // tasks/{id} + GET_ALL_TASKS, // tasks + POST_NEW_TASK, // tasks/{id} + POST_UPDATE_TASK, // tasks + DELETE_TASK, // tasks/{id} + + // Подзадачи + GET_SUBTASK, // subtasks/{id} + GET_ALL_SUBTASKS, // subtasks + POST_NEW_SUBTASK, // subtasks + POST_UPDATE_SUBTASK, // subtasks/{id} + DELETE_SUBTASK, // subtasks{id} + + GET_ALL_SUBTASKS_BY_EPIC, // epic/{id}/subtasks + + // Эпики + GET_EPIC, // epics/{id} + GET_ALL_EPICS, // epics + POST_NEW_EPIC, // epics + POST_UPDATE_EPIC, // epics/{id} + DELETE_EPIC, // epics/{id} + + // История + GET_HISTORY, // history + + // Приоритеты + GET_PRIORITIZED, // prioritized + + // Пустой + UNKNOWN +} diff --git a/src/data/Property.java b/src/data/Property.java new file mode 100644 index 0000000..e5dccb0 --- /dev/null +++ b/src/data/Property.java @@ -0,0 +1,12 @@ +package data; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.format.DateTimeFormatter; + +public interface Property { + Charset CHARSET = StandardCharsets.UTF_8; + DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + int PORT = 8080; + +} diff --git a/src/data/Task.java b/src/data/Task.java index ba57ee2..542f202 100644 --- a/src/data/Task.java +++ b/src/data/Task.java @@ -25,6 +25,7 @@ public class Task implements Comparable { /// Продолжительность задачи в минутах protected Duration duration; + /// Формат даты и времени protected static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); /// Конструктор 1 diff --git a/src/manager/FileBackedTaskManager.java b/src/manager/FileBackedTaskManager.java index 3b5265f..43dc13a 100644 --- a/src/manager/FileBackedTaskManager.java +++ b/src/manager/FileBackedTaskManager.java @@ -4,16 +4,13 @@ import exception.ManagerSaveException; import java.io.*; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.*; -public class FileBackedTaskManager extends InMemoryTaskManager { +public class FileBackedTaskManager extends InMemoryTaskManager implements Property { private static File file; private static FileBackedTaskManager instance; @@ -21,8 +18,6 @@ public class FileBackedTaskManager extends InMemoryTaskManager { /// Параметры CSV файла // 0 1 2 3 4 5 6 7 8 private static final String TABLE_HEADER = "id,type,name,status,description,start,end,duration,epic"; - private static final Charset CHARSET = StandardCharsets.UTF_8; - private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); private static final String DELIMITER = ","; private static final String QUOTE = "\""; diff --git a/src/manager/InMemoryTaskManager.java b/src/manager/InMemoryTaskManager.java index 63b35fe..02633df 100644 --- a/src/manager/InMemoryTaskManager.java +++ b/src/manager/InMemoryTaskManager.java @@ -7,7 +7,9 @@ import java.util.*; import java.util.stream.Stream; -class InMemoryTaskManager implements TaskManager { +public class InMemoryTaskManager implements TaskManager { + + private static InMemoryTaskManager instance; /// Хранилище задач protected final Map idToTask = new HashMap<>(); @@ -32,7 +34,18 @@ private int generateNewId() { return counter++; } - /// Получить объект по id с сохранением в историю + /// Конструктор + public InMemoryTaskManager() { + } + + /// Получить синглтон класса + public static InMemoryTaskManager getInstance() { + if (instance == null) { + instance = new InMemoryTaskManager(); + } + return instance; + } + @Override public Task get(int id) { if (idToTask.containsKey(id)) { @@ -52,7 +65,6 @@ public Task get(int id) { return null; } - /// Получить объект по id без сохранения в историю @Override public Task getWithoutHistory(int id) { if (idToTask.containsKey(id)) { @@ -66,7 +78,6 @@ public Task getWithoutHistory(int id) { return null; } - /// Получить тип объекта по id @Override public Type getType(int id) { Task taskObject = getWithoutHistory(id); @@ -76,7 +87,6 @@ public Type getType(int id) { return taskObject.getType(); } - /// Получить все объекты из всех хранилищ public TreeSet getAll() { if (counter == 1) { return new TreeSet<>(); @@ -89,7 +99,6 @@ public TreeSet getAll() { return new TreeSet<>(all); } - /// Внесение задачи в хранилище по объекту @Override public Task addTask(Task newTask) { if (newTask.getType() != Type.TASK) { @@ -116,7 +125,6 @@ public Task addTask(Task newTask) { return newTask; } - /// Внесение эпика в хранилище по объекту @Override public Epic addEpic(Epic newEpic) { if (newEpic.getType() != Type.EPIC) { @@ -137,7 +145,6 @@ public Epic addEpic(Epic newEpic) { return newEpic; } - /// Внесение подзадачи в хранилище по объекту @Override public Subtask addSubtask(Subtask newSubtask) { Integer epicId = newSubtask.getEpicId(); @@ -172,7 +179,6 @@ public Subtask addSubtask(Subtask newSubtask) { return newSubtask; } - /// Получение задачи по ID с сохранением в историю @Override public Task getTask(int taskID) { Task newTask = copyOfTask(taskID); @@ -183,7 +189,6 @@ public Task getTask(int taskID) { return newTask; } - /// Получение эпика по ID с сохранением в историю @Override public Epic getEpic(int epicID) { Epic newEpic = copyOfEpic(epicID); @@ -194,7 +199,6 @@ public Epic getEpic(int epicID) { return newEpic; } - /// Получение подзадачи по ID с сохранением в историю @Override public Subtask getSubtask(int subTaskID) { Subtask newSubtask = copyOfSubtask(subTaskID); @@ -229,25 +233,22 @@ private Subtask copyOfSubtask(int subtaskId) { return idToSubtask.get(subtaskId).getCopy(); } - /// Получение списка всех задач в виде объекта @Override public List getAllTasks() { return idToTask.values().stream().sorted(Task::compareTo).toList(); } - /// Получение списка всех эпиков в виде объекта + @Override public List getAllEpics() { return idToEpic.values().stream().sorted(Task::compareTo).toList(); } - /// Получение списка всех подзадач в виде объекта @Override public List getAllSubtasks() { return idToSubtask.values().stream().sorted(Task::compareTo).toList(); } - /// Получение списка всех подзадач эпика в виде объекта @Override public List getAllSubTasksByEpic(int epicId) { if (!idToEpic.containsKey(epicId)) { @@ -259,7 +260,11 @@ public List getAllSubTasksByEpic(int epicId) { .toList(); } - /// Обновление задачи по объекту + @Override + public List getPrioritizedTasks() { + return new ArrayList<>(prioritizedTasks); + } + @Override public Task updateTask(Task newTask) { Integer id = newTask.getId(); @@ -312,7 +317,6 @@ public Task updateTask(Task newTask) { return newTask; } - /// Обновление эпика по объекту @Override public Epic updateEpic(Epic newEpic) { Integer id = newEpic.getId(); @@ -336,7 +340,6 @@ public Epic updateEpic(Epic newEpic) { return newEpic; } - /// Обновление подзадачи по объекту @Override public Subtask updateSubtask(Subtask newSubtask) { Integer subtaskId = newSubtask.getId(); @@ -514,7 +517,6 @@ public List findIntersections(Task taskObject) { .toList(); } - /// Удаление задачи по ID @Override public boolean removeTask(int taskId) { if (!idToTask.containsKey(taskId)) { @@ -526,7 +528,6 @@ public boolean removeTask(int taskId) { return true; } - /// Удаление эпика по ID @Override public boolean removeEpic(int epicId) { if (!idToEpic.containsKey(epicId)) { @@ -538,7 +539,6 @@ public boolean removeEpic(int epicId) { return true; } - /// Удаление подзадачи по ID @Override public boolean removeSubtask(int subtaskId) { if (!idToSubtask.containsKey(subtaskId)) { @@ -556,7 +556,6 @@ public boolean removeSubtask(int subtaskId) { return true; } - /// Удаление всех подзадач эпика по ID эпика @Override public void removeAllSubtasksByEpic(int epicId) { if (!idToEpic.containsKey(epicId)) { @@ -573,7 +572,6 @@ public void removeAllSubtasksByEpic(int epicId) { clearEpicTime(epic); } - /// Удаление всех задач @Override public void removeAllTasks() { for (int taskId : idToTask.keySet()) { @@ -583,7 +581,6 @@ public void removeAllTasks() { idToTask.clear(); } - /// Удаление всех эпиков @Override public void removeAllEpics() { for (int epicId : idToEpic.keySet()) { @@ -598,7 +595,6 @@ public void removeAllEpics() { idToSubtask.clear(); } - /// Удаление всех подзадач @Override public void removeAllSubTasks() { for (int subtaskId : idToSubtask.keySet()) { @@ -614,7 +610,6 @@ public void removeAllSubTasks() { } } - /// Получить список с историей @Override public List getHistory() { return historyManager.getHistory(); diff --git a/src/manager/Managers.java b/src/manager/Managers.java index f4ab859..bbd2726 100644 --- a/src/manager/Managers.java +++ b/src/manager/Managers.java @@ -1,13 +1,20 @@ package manager; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import server.adapters.DurationAdapter; +import server.adapters.LocalDateTimeAdapter; + import java.io.File; +import java.time.Duration; +import java.time.LocalDateTime; public class Managers { public static TaskManager getTaskManager(TaskManagerType type) { switch (type) { case IN_MEMORY -> { - return new InMemoryTaskManager(); + return InMemoryTaskManager.getInstance(); } case FILE_BACKED -> { return FileBackedTaskManager.getInstance(new File( @@ -24,4 +31,13 @@ public static TaskManager getTaskManager(TaskManagerType type) { public static HistoryManager getHistoryManager() { return new InMemoryHistoryManager(); } + + public static Gson getGson() { + return new GsonBuilder() + .setPrettyPrinting() + .serializeNulls() + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter()) + .registerTypeAdapter(Duration.class, new DurationAdapter()) + .create(); + } } diff --git a/src/manager/TaskManager.java b/src/manager/TaskManager.java index 7b4998d..73354a0 100644 --- a/src/manager/TaskManager.java +++ b/src/manager/TaskManager.java @@ -10,53 +10,80 @@ public interface TaskManager { + /// Внесение задачи в хранилище по объекту Task addTask(Task newTask); + /// Внесение эпика в хранилище по объекту Epic addEpic(Epic newEpic); + /// Внесение подзадачи в хранилище по объекту Subtask addSubtask(Subtask newSubtask); + /// Получение задачи по ID с сохранением в историю Task getTask(int taskID); + /// Получение эпика по ID с сохранением в историю Epic getEpic(int epicID); + /// Получение подзадачи по ID с сохранением в историю Subtask getSubtask(int subTaskID); + /// Получение списка всех задач в виде объекта List getAllTasks(); + /// Получение списка всех эпиков в виде объекта List getAllEpics(); + /// Получение списка всех подзадач в виде объекта List getAllSubtasks(); + /// Получение списка всех подзадач эпика в виде объекта List getAllSubTasksByEpic(int epicId); + List getPrioritizedTasks(); + + /// Обновление задачи по объекту, в случае неудачи возвращает null Task updateTask(Task newTask); + /// Обновление эпика по объекту, в случае неудачи возвращает null Epic updateEpic(Epic newEpic); + /// Обновление подзадачи по объекту, в случае неудачи возвращает null Subtask updateSubtask(Subtask newSubtask); + /// Удаление задачи по ID boolean removeTask(int taskId); + /// Удаление эпика по ID boolean removeEpic(int epicId); + /// Удаление подзадачи по ID boolean removeSubtask(int subtaskId); + /// Удаление всех подзадач эпика по ID эпика void removeAllSubtasksByEpic(int epicId); + /// Удаление всех задач void removeAllTasks(); + /// Удаление всех эпиков void removeAllEpics(); + /// Удаление всех подзадач void removeAllSubTasks(); + /// Получить список с историей List getHistory(); + /// Получить все объекты из всех хранилищ TreeSet getAll(); + /// Получить объект по id с сохранением в историю Task get(int id); + /// Получить объект по id без сохранения в историю Task getWithoutHistory(int id); + /// Получить тип объекта по id Type getType(int id); } diff --git a/src/server/HttpTaskServer.java b/src/server/HttpTaskServer.java new file mode 100644 index 0000000..fb0c6a2 --- /dev/null +++ b/src/server/HttpTaskServer.java @@ -0,0 +1,46 @@ +package server; + +import com.sun.net.httpserver.HttpServer; +import data.Property; +import manager.Managers; +import manager.TaskManager; + +import java.io.IOException; +import java.net.InetSocketAddress; + +import manager.TaskManagerType; +import server.handlers.*; + +public class HttpTaskServer implements Property { + + private static HttpServer httpServer; + private static TaskManager manager; + + HttpTaskServer(TaskManager manager) throws IOException { + HttpTaskServer.manager = manager; + httpServer = HttpServer.create(); + } + + public static void main(String[] args) throws IOException { + HttpTaskServer server = new HttpTaskServer(Managers.getTaskManager(TaskManagerType.FILE_BACKED)); + server.start(); + } + + public void start() throws IOException { + httpServer.bind(new InetSocketAddress(PORT), 0); + httpServer.createContext("/tasks", new TasksHandler(manager)); + httpServer.createContext("/subtasks", new SubtasksHandler(manager)); + httpServer.createContext("/epics", new EpicsHandler(manager)); + httpServer.createContext("/history", new HistoryHandler(manager)); + httpServer.createContext("/prioritized", new PrioritizedHandler(manager)); + httpServer.createContext("/", new BaseHttpHandler(manager)); + httpServer.start(); + + System.out.println("HTTP-сервер запущен на http://localhost:" + PORT + '/'); + } + + public void stop() { + httpServer.stop(1); + } + +} diff --git a/src/server/adapters/DurationAdapter.java b/src/server/adapters/DurationAdapter.java new file mode 100644 index 0000000..b3b45de --- /dev/null +++ b/src/server/adapters/DurationAdapter.java @@ -0,0 +1,30 @@ +package server.adapters; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.time.Duration; + +public class DurationAdapter extends TypeAdapter { + @Override + public void write(final JsonWriter jsonWriter, final Duration duration) throws IOException { + if (duration == null) { + jsonWriter.nullValue(); + return; + } + jsonWriter.value(duration.toMinutes()); + } + + @Override + public Duration read(final JsonReader jsonReader) throws IOException { + JsonToken peek = jsonReader.peek(); + if (peek.name().equals("NULL")) { + jsonReader.nextNull(); + return null; + } + return Duration.ofMinutes(jsonReader.nextInt()); + } +} diff --git a/src/server/adapters/LocalDateTimeAdapter.java b/src/server/adapters/LocalDateTimeAdapter.java new file mode 100644 index 0000000..7617909 --- /dev/null +++ b/src/server/adapters/LocalDateTimeAdapter.java @@ -0,0 +1,31 @@ +package server.adapters; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import data.Property; + +import java.io.IOException; +import java.time.LocalDateTime; + +public class LocalDateTimeAdapter extends TypeAdapter implements Property { + @Override + public void write(final JsonWriter jsonWriter, final LocalDateTime dateTime) throws IOException { + if (dateTime == null) { + jsonWriter.nullValue(); + return; + } + jsonWriter.value(dateTime.format(DATE_TIME_FORMATTER)); + } + + @Override + public LocalDateTime read(final JsonReader jsonReader) throws IOException { + JsonToken peek = jsonReader.peek(); + if (peek.name().equals("NULL")) { + jsonReader.nextNull(); + return null; + } + return LocalDateTime.parse(jsonReader.nextString(), DATE_TIME_FORMATTER); + } +} diff --git a/src/server/handlers/BaseHttpHandler.java b/src/server/handlers/BaseHttpHandler.java new file mode 100644 index 0000000..063efc4 --- /dev/null +++ b/src/server/handlers/BaseHttpHandler.java @@ -0,0 +1,199 @@ +package server.handlers; + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import data.Endpoint; +import data.Property; +import data.Task; +import manager.Managers; +import manager.TaskManager; + +import java.io.IOException; +import java.util.regex.Pattern; + +public class BaseHttpHandler implements HttpHandler, Property { + + protected final TaskManager manager; + protected static Gson gson = Managers.getGson(); + + public BaseHttpHandler(TaskManager manager) { + this.manager = manager; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + String method = exchange.getRequestMethod(); + String path = exchange.getRequestURI().getPath(); + + Endpoint endpoint = getEndpoint(method, path); + + switch (endpoint) { + case BASE -> { + String text = + "\tWelcome to java server.\n" + + "\tUse next endpoints:\n" + + "\thttp://localhost:" + PORT + "/tasks\n" + + "\thttp://localhost:" + PORT + "/subtasks\n" + + "\thttp://localhost:" + PORT + "/epics\n" + + "\thttp://localhost:" + PORT + "/history\n" + + "\thttp://localhost:" + PORT + ":/prioritized"; + + sendText(exchange, Endpoint.BASE, text); + } + + case UNKNOWN -> sendFormatException(exchange, Endpoint.UNKNOWN, path); + } + } + + /// Для отправки общего ответа в случае успеха + protected void sendText(HttpExchange exchange, Endpoint endpoint, String text) throws IOException { + byte[] resp = text.getBytes(CHARSET); + exchange.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); + exchange.sendResponseHeaders(200, resp.length); + exchange.getResponseBody().write(resp); + System.out.println("\t" + endpoint + " 200 success"); + exchange.close(); + } + + ///Сервер обнаружил в запросе клиента синтаксическую ошибку. + protected void sendBadRequestBoby(HttpExchange exchange, Endpoint endpoint) throws IOException { + String text = ("\t" + endpoint + " 400 bad request body"); + byte[] resp = text.getBytes(CHARSET); + exchange.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); + exchange.sendResponseHeaders(400, resp.length); + exchange.getResponseBody().write(resp); + System.out.println(text); + exchange.close(); + } + + /// Для отправки ответа в случае, если объект не был найден + protected void sendNotFound(HttpExchange exchange, Endpoint endpoint) throws IOException { + String text = ("\t" + endpoint + " 404 not found"); + byte[] resp = text.getBytes(CHARSET); + exchange.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); + exchange.sendResponseHeaders(404, resp.length); + exchange.getResponseBody().write(resp); + System.out.println(text); + exchange.close(); + } + + /// Сервер понял запрос, но метод, использованный клиентом (например, GET или POST), не разрешен для данного ресурса + protected void sendFormatException(HttpExchange exchange, Endpoint endpoint, String path) throws IOException { + String text = ("\t" + endpoint + " 405 path format exception: " + path); + byte[] resp = text.getBytes(CHARSET); + exchange.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); + exchange.sendResponseHeaders(405, resp.length); + exchange.getResponseBody().write(resp); + System.out.println(text); + exchange.close(); + } + + /// Запрошенный URI не может удовлетворить переданным в заголовке характеристикам. + protected void sendHasOverlaps(HttpExchange exchange, Endpoint endpoint, Task taskObject) throws IOException { + String text = ("\t" + endpoint + " 406 time overlaps: " + + taskObject.getStartTime().format(DATE_TIME_FORMATTER) + " - " + + taskObject.getEndTime().format(DATE_TIME_FORMATTER)); + + byte[] resp = text.getBytes(CHARSET); + exchange.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); + exchange.sendResponseHeaders(406, resp.length); + exchange.getResponseBody().write(resp); + System.out.println(text); + exchange.close(); + } + + /** + * Сервер успешно принял запрос, может работать с указанным видом данных, + * однако имеется какая-то логическая ошибка, из-за которой невозможно произвести операцию над ресурсом. + */ + protected void sendUnprocessableEntity(HttpExchange exchange, Endpoint endpoint) throws IOException { + String text = ("\t" + endpoint + " 422 unprocessable entity"); + byte[] resp = text.getBytes(CHARSET); + exchange.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); + exchange.sendResponseHeaders(422, resp.length); + exchange.getResponseBody().write(resp); + System.out.println(text); + exchange.close(); + } + + /// Для отправки ответа, если произошла ошибка на сервере + protected void sendServerError(HttpExchange exchange, Endpoint endpoint) throws IOException { + String text = ("\t" + endpoint + " 500 Internal Server Error"); + byte[] resp = text.getBytes(CHARSET); + exchange.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); + exchange.sendResponseHeaders(500, resp.length); + exchange.getResponseBody().write(resp); + System.out.println(text); + exchange.close(); + } + + protected static int parsePathId(String pathId) { + try { + return Integer.parseInt(pathId); + } catch (NumberFormatException exception) { + return -1; + } + } + + protected Endpoint getEndpoint(String method, String path) { + + switch (method) { + case "GET": { + if (Pattern.matches("^/$", path)) { + return Endpoint.BASE; + } + + if (Pattern.matches("^/tasks/\\d+$", path)) { + return Endpoint.GET_TASK; + } else if (Pattern.matches("^/tasks$", path)) { + return Endpoint.GET_ALL_TASKS; + } else if (Pattern.matches("^/subtasks/\\d+$", path)) { + return Endpoint.GET_SUBTASK; + } else if (Pattern.matches("^/subtasks$", path)) { + return Endpoint.GET_ALL_SUBTASKS; + } else if (Pattern.matches("^/epics/\\d+/subtasks$", path)) { + return Endpoint.GET_ALL_SUBTASKS_BY_EPIC; + } else if (Pattern.matches("^/epics/\\d+$", path)) { + return Endpoint.GET_EPIC; + } else if (Pattern.matches("^/epics$", path)) { + return Endpoint.GET_ALL_EPICS; + } else if (Pattern.matches("^/history$", path)) { + return Endpoint.GET_HISTORY; + } else if (Pattern.matches("^/prioritized$", path)) { + return Endpoint.GET_PRIORITIZED; + } + break; + } + case "POST": { + if (Pattern.matches("^/tasks/\\d+$", path)) { + return Endpoint.POST_UPDATE_TASK; + } else if (Pattern.matches("^/tasks$", path)) { + return Endpoint.POST_NEW_TASK; + } else if (Pattern.matches("^/subtasks/\\d+$", path)) { + return Endpoint.POST_UPDATE_SUBTASK; + } else if (Pattern.matches("^/subtasks$", path)) { + return Endpoint.POST_NEW_SUBTASK; + } else if (Pattern.matches("^/epics/\\d+$", path)) { + return Endpoint.POST_UPDATE_EPIC; + } else if (Pattern.matches("^/epics$", path)) { + return Endpoint.POST_NEW_EPIC; + } + break; + } + case "DELETE": { + if (Pattern.matches("^/tasks/\\d+$", path)) { + return Endpoint.DELETE_TASK; + } else if (Pattern.matches("^/subtasks/\\d+$", path)) { + return Endpoint.DELETE_SUBTASK; + } else if (Pattern.matches("^/epics/\\d+$", path)) { + return Endpoint.DELETE_EPIC; + } + break; + } + } + + return Endpoint.UNKNOWN; + } + +} diff --git a/src/server/handlers/EpicsHandler.java b/src/server/handlers/EpicsHandler.java new file mode 100644 index 0000000..d7aeb4d --- /dev/null +++ b/src/server/handlers/EpicsHandler.java @@ -0,0 +1,188 @@ +package server.handlers; + +import com.sun.net.httpserver.HttpExchange; +import data.Endpoint; +import data.Epic; +import data.Subtask; +import manager.TaskManager; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +public class EpicsHandler extends BaseHttpHandler { + + public EpicsHandler(TaskManager manager) { + super(manager); + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + String method = exchange.getRequestMethod(); + String path = exchange.getRequestURI().getPath(); + + Endpoint endpoint = getEndpoint(method, path); + + switch (endpoint) { + + case GET_EPIC -> { + + String pathId = path.replaceFirst("/epics/", ""); + int id = parsePathId(pathId); + if (id == -1) { + sendFormatException(exchange, endpoint, path); + return; + } + + Epic epic = manager.getEpic(id); + if (epic == null) { + sendNotFound(exchange, endpoint); + return; + } + + String jsonEpic; + try { + jsonEpic = gson.toJson(epic); + } catch (Exception exception) { + sendServerError(exchange, endpoint); + System.out.println(exception.getMessage()); + return; + } + + sendText(exchange, endpoint, jsonEpic); + + } + + case GET_ALL_EPICS -> { + + List epicList = manager.getAllEpics(); + if (epicList.isEmpty()) { + sendNotFound(exchange, endpoint); + return; + } + + String jsonEpicList; + try { + jsonEpicList = gson.toJson(epicList); + } catch (Exception exception) { + sendServerError(exchange, endpoint); + System.out.println(exception.getMessage()); + return; + } + + sendText(exchange, endpoint, jsonEpicList); + + } + + case POST_NEW_EPIC -> { + + InputStream bodyInputStream = exchange.getRequestBody(); + String body = new String(bodyInputStream.readAllBytes(), CHARSET); + + Epic epic; + try { + epic = gson.fromJson(body, Epic.class); + } catch (Exception e) { + sendBadRequestBoby(exchange, endpoint); + System.out.println(e.getMessage()); + return; + } + + Integer epicId; + try { + epicId = manager.addEpic(epic).getId(); + } catch (Exception exception) { + sendServerError(exchange, endpoint); + System.out.println(exception.getMessage()); + return; + } + + sendText(exchange, endpoint, "epic adding success, epicId=" + epicId); + + } + + case POST_UPDATE_EPIC -> { + + String pathId = path.replaceFirst("/epics/", ""); + int id = parsePathId(pathId); + if (id == -1) { + sendFormatException(exchange, endpoint, path); + return; + } + + InputStream bodyInputStream = exchange.getRequestBody(); + String body = new String(bodyInputStream.readAllBytes(), CHARSET); + + Epic epic; + try { + epic = gson.fromJson(body, Epic.class); + } catch (Exception e) { + sendBadRequestBoby(exchange, endpoint); + System.out.println(e.getMessage()); + return; + } + + Integer epicId = epic.getId(); + if (epicId == null || id != epicId) { + sendUnprocessableEntity(exchange, endpoint); + return; + } + + if (manager.updateEpic(epic) == null) { + sendNotFound(exchange, endpoint); + } else { + sendText(exchange, endpoint, "epic updated success"); + } + + } + + case DELETE_EPIC -> { + + String pathId = path.replaceFirst("/epics/", ""); + int id = parsePathId(pathId); + if (id == -1) { + sendFormatException(exchange, endpoint, path); + return; + } + + if (manager.removeEpic(id)) { + sendText(exchange, endpoint, "epic removed success"); + } else { + sendNotFound(exchange, endpoint); + } + + } + + case GET_ALL_SUBTASKS_BY_EPIC -> { + + String pathId = path.replaceFirst("/epics/", ""); + pathId = pathId.replaceFirst("/subtasks", ""); + int id = parsePathId(pathId); + if (id == -1) { + sendFormatException(exchange, endpoint, path); + return; + } + + List subtaskList = manager.getAllSubTasksByEpic(id); + if (subtaskList.isEmpty()) { + sendNotFound(exchange, endpoint); + return; + } + + String jsonSubtaskList; + try { + jsonSubtaskList = gson.toJson(subtaskList); + } catch (Exception exception) { + sendServerError(exchange, endpoint); + System.out.println(exception.getMessage()); + return; + } + + sendText(exchange, endpoint, jsonSubtaskList); + + } + + case UNKNOWN -> sendFormatException(exchange, endpoint, path); + } + } +} diff --git a/src/server/handlers/HistoryHandler.java b/src/server/handlers/HistoryHandler.java new file mode 100644 index 0000000..2089bf2 --- /dev/null +++ b/src/server/handlers/HistoryHandler.java @@ -0,0 +1,50 @@ +package server.handlers; + +import com.sun.net.httpserver.HttpExchange; +import data.Endpoint; +import data.Task; +import manager.TaskManager; + +import java.io.IOException; +import java.util.List; + +public class HistoryHandler extends BaseHttpHandler { + + public HistoryHandler(TaskManager manager) { + super(manager); + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + String method = exchange.getRequestMethod(); + String path = exchange.getRequestURI().getPath(); + + Endpoint endpoint = getEndpoint(method, path); + + switch (endpoint) { + + case GET_HISTORY -> { + + List historyList = manager.getHistory(); + if (historyList.isEmpty()) { + sendNotFound(exchange, endpoint); + return; + } + + String jsonHistoryList; + try { + jsonHistoryList = gson.toJson(historyList); + } catch (Exception exception) { + sendServerError(exchange, endpoint); + System.out.println(exception.getMessage()); + return; + } + + sendText(exchange, endpoint, jsonHistoryList); + + } + + case UNKNOWN -> sendFormatException(exchange, endpoint, path); + } + } +} diff --git a/src/server/handlers/PrioritizedHandler.java b/src/server/handlers/PrioritizedHandler.java new file mode 100644 index 0000000..37820c4 --- /dev/null +++ b/src/server/handlers/PrioritizedHandler.java @@ -0,0 +1,50 @@ +package server.handlers; + +import com.sun.net.httpserver.HttpExchange; +import data.Endpoint; +import data.Task; +import manager.TaskManager; + +import java.io.IOException; +import java.util.List; + +public class PrioritizedHandler extends BaseHttpHandler { + + public PrioritizedHandler(TaskManager manager) { + super(manager); + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + String method = exchange.getRequestMethod(); + String path = exchange.getRequestURI().getPath(); + + Endpoint endpoint = getEndpoint(method, path); + + switch (endpoint) { + + case GET_PRIORITIZED -> { + + List prioritizedList = manager.getPrioritizedTasks(); + if (prioritizedList.isEmpty()) { + sendNotFound(exchange, endpoint); + return; + } + + String jsonPrioritizedList; + try { + jsonPrioritizedList = gson.toJson(prioritizedList); + } catch (Exception exception) { + sendServerError(exchange, endpoint); + System.out.println(exception.getMessage()); + return; + } + + sendText(exchange, endpoint, jsonPrioritizedList); + + } + + case UNKNOWN -> sendFormatException(exchange, endpoint, path); + } + } +} diff --git a/src/server/handlers/SubtasksHandler.java b/src/server/handlers/SubtasksHandler.java new file mode 100644 index 0000000..70addc8 --- /dev/null +++ b/src/server/handlers/SubtasksHandler.java @@ -0,0 +1,165 @@ +package server.handlers; + +import com.sun.net.httpserver.HttpExchange; +import data.Endpoint; +import data.Subtask; +import manager.TaskManager; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +public class SubtasksHandler extends BaseHttpHandler { + + public SubtasksHandler(TaskManager manager) { + super(manager); + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + String method = exchange.getRequestMethod(); + String path = exchange.getRequestURI().getPath(); + + Endpoint endpoint = getEndpoint(method, path); + + switch (endpoint) { + + case GET_SUBTASK -> { + + String pathId = path.replaceFirst("/subtasks/", ""); + int id = parsePathId(pathId); + if (id == -1) { + sendFormatException(exchange, endpoint, path); + return; + } + + Subtask subtask = manager.getSubtask(id); + if (subtask == null) { + sendNotFound(exchange, endpoint); + return; + } + + String jsonSubtask; + try { + jsonSubtask = gson.toJson(subtask); + } catch (Exception exception) { + sendServerError(exchange, endpoint); + System.out.println(exception.getMessage()); + return; + } + + sendText(exchange, endpoint, jsonSubtask); + + } + + case GET_ALL_SUBTASKS -> { + + List subtaskList = manager.getAllSubtasks(); + if (subtaskList.isEmpty()) { + sendNotFound(exchange, endpoint); + return; + } + + String jsonSubtaskList; + try { + jsonSubtaskList = gson.toJson(subtaskList); + } catch (Exception exception) { + sendServerError(exchange, endpoint); + System.out.println(exception.getMessage()); + return; + } + + sendText(exchange, endpoint, jsonSubtaskList); + + } + + case POST_NEW_SUBTASK -> { + + InputStream bodyInputStream = exchange.getRequestBody(); + String body = new String(bodyInputStream.readAllBytes(), CHARSET); + + Subtask subtask; + try { + subtask = gson.fromJson(body, Subtask.class); + } catch (Exception e) { + sendBadRequestBoby(exchange, endpoint); + System.out.println(e.getMessage()); + return; + } + + Integer epicId = subtask.getEpicId(); + if (epicId == null) { + sendUnprocessableEntity(exchange, endpoint); + } else if (manager.getWithoutHistory(epicId) == null) { + sendNotFound(exchange, endpoint); + return; + } + + Subtask newSubtask = manager.addSubtask(subtask); + + if (newSubtask == null) { + sendHasOverlaps(exchange, endpoint, subtask); + } else { + sendText(exchange, endpoint, "subtask adding success, subtaskId=" + newSubtask.getId()); + } + + } + + case POST_UPDATE_SUBTASK -> { + + String pathId = path.replaceFirst("/subtasks/", ""); + int id = parsePathId(pathId); + if (id == -1) { + sendFormatException(exchange, endpoint, path); + return; + } + + InputStream bodyInputStream = exchange.getRequestBody(); + String body = new String(bodyInputStream.readAllBytes(), CHARSET); + + Subtask subtask; + try { + subtask = gson.fromJson(body, Subtask.class); + } catch (Exception e) { + sendBadRequestBoby(exchange, endpoint); + System.out.println(e.getMessage()); + return; + } + + Integer subtaskId = subtask.getId(); + if (subtaskId == null || id != subtaskId) { + sendUnprocessableEntity(exchange, endpoint); + } else if (manager.getWithoutHistory(subtaskId) == null) { + sendNotFound(exchange, endpoint); + return; + } + + if (manager.updateSubtask(subtask) == null) { + sendHasOverlaps(exchange, endpoint, subtask); + } else { + sendText(exchange, endpoint, "subtask updated success"); + } + + } + + case DELETE_SUBTASK -> { + + String pathId = path.replaceFirst("/subtasks/", ""); + int id = parsePathId(pathId); + if (id == -1) { + sendFormatException(exchange, endpoint, path); + return; + } + + if (manager.removeSubtask(id)) { + sendText(exchange, endpoint, "subtask removed success"); + } else { + sendNotFound(exchange, endpoint); + } + + } + + case UNKNOWN -> sendFormatException(exchange, endpoint, path); + } + } +} diff --git a/src/server/handlers/TasksHandler.java b/src/server/handlers/TasksHandler.java new file mode 100644 index 0000000..0614e50 --- /dev/null +++ b/src/server/handlers/TasksHandler.java @@ -0,0 +1,160 @@ +package server.handlers; + +import com.sun.net.httpserver.HttpExchange; +import data.Endpoint; +import data.Task; +import manager.TaskManager; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +public class TasksHandler extends BaseHttpHandler { + + public TasksHandler(TaskManager manager) { + super(manager); + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + String method = exchange.getRequestMethod(); + String path = exchange.getRequestURI().getPath(); + + Endpoint endpoint = getEndpoint(method, path); + + switch (endpoint) { + + case GET_TASK -> { + + String pathId = path.replaceFirst("/tasks/", ""); + int id = parsePathId(pathId); + if (id == -1) { + sendFormatException(exchange, endpoint, path); + return; + } + + Task task = manager.getTask(id); + if (task == null) { + sendNotFound(exchange, endpoint); + return; + } + + String jsonTask; + try { + jsonTask = gson.toJson(task); + } catch (Exception exception) { + sendServerError(exchange, endpoint); + System.out.println(exception.getMessage()); + return; + } + + sendText(exchange, endpoint, jsonTask); + + } + + case GET_ALL_TASKS -> { + + List taskList = manager.getAllTasks(); + if (taskList.isEmpty()) { + sendNotFound(exchange, endpoint); + return; + } + + String jsonTaskList; + try { + jsonTaskList = gson.toJson(taskList); + } catch (Exception exception) { + sendServerError(exchange, endpoint); + System.out.println(exception.getMessage()); + return; + } + sendText(exchange, endpoint, jsonTaskList); + + } + + case POST_NEW_TASK -> { + + InputStream bodyInputStream = exchange.getRequestBody(); + String body = new String(bodyInputStream.readAllBytes(), CHARSET); + + Task task; + try { + task = gson.fromJson(body, Task.class); + } catch (Exception e) { + sendBadRequestBoby(exchange, endpoint); + System.out.println(e.getMessage()); + return; + } + + Task newTask = manager.addTask(task); + + if (newTask == null) { + sendHasOverlaps(exchange, endpoint, task); + } else { + sendText(exchange, endpoint,"task adding success, taskId=" + newTask.getId()); + } + + } + + case POST_UPDATE_TASK -> { + + String pathId = path.replaceFirst("/tasks/", ""); + int id = parsePathId(pathId); + if (id == -1) { + sendFormatException(exchange, endpoint, path); + return; + } + + InputStream bodyInputStream = exchange.getRequestBody(); + String body = new String(bodyInputStream.readAllBytes(), CHARSET); + + Task task; + try { + task = gson.fromJson(body, Task.class); + } catch (Exception e) { + sendBadRequestBoby(exchange, endpoint); + System.out.println(e.getMessage()); + return; + } + + Integer taskId = task.getId(); + if (taskId == null || id != taskId) { + sendUnprocessableEntity(exchange, endpoint); + return; + } else if (manager.getWithoutHistory(taskId) == null) { + sendNotFound(exchange, endpoint); + return; + } + + if (manager.updateTask(task) == null) { + sendHasOverlaps(exchange, endpoint, task); + } else { + sendText(exchange, endpoint,"task updated success"); + } + + } + + case DELETE_TASK -> { + + String pathId = path.replaceFirst("/tasks/", ""); + int id = parsePathId(pathId); + if (id == -1) { + sendFormatException(exchange, endpoint, path); + return; + } + + if (manager.removeTask(id)) { + sendText(exchange, endpoint, "task removed success"); + } else { + sendNotFound(exchange, endpoint); + } + + } + + case UNKNOWN -> sendFormatException(exchange, endpoint, path); + } + + + } + +} diff --git a/test/manager/TaskManagerTest.java b/test/manager/TaskManagerTest.java index fa2186e..5f51124 100644 --- a/test/manager/TaskManagerTest.java +++ b/test/manager/TaskManagerTest.java @@ -40,7 +40,7 @@ void updateTaskHistory(List history) { @BeforeEach void beforeEach() { // Создать новый менеджер задач - manager = Managers.getTaskManager(TaskManagerType.IN_MEMORY); + manager = new InMemoryTaskManager(); // Создать новую задачу task = new Task( diff --git a/test/server/HttpTaskServerTest.java b/test/server/HttpTaskServerTest.java new file mode 100644 index 0000000..a12853f --- /dev/null +++ b/test/server/HttpTaskServerTest.java @@ -0,0 +1,95 @@ +package server; + +import com.google.gson.Gson; +import data.Property; +import data.Task; +import manager.InMemoryTaskManager; +import manager.Managers; +import manager.TaskManager; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public abstract class HttpTaskServerTest implements Property { + protected TaskManager manager; + protected HttpTaskServer taskServer; + protected Gson gson; + protected final String host = "http://localhost:" + PORT + '/'; + + protected Task task1; + protected Task task2; + + @BeforeEach + void beforeEach() throws IOException { + manager = new InMemoryTaskManager(); + taskServer = new HttpTaskServer(manager); + gson = Managers.getGson(); + taskServer.start(); + + task1 = new Task(null, "FIRST TASK", "first task"); + task2 = new Task(null, "SECOND TASK", "second task"); + manager.addTask(task1); + manager.addTask(task2); + } + + @AfterEach + void afterEach() { + taskServer.stop(); + } + + /// Отправляет GET-запрос и возвращает ответ сервера + protected HttpResponse responseOfNewGetRequest(String stringUri) { + URI uri = URI.create(host + stringUri); + + try (HttpClient client = HttpClient.newHttpClient()) { + HttpRequest request = HttpRequest.newBuilder().GET().uri(uri).build(); + HttpResponse.BodyHandler handler = HttpResponse.BodyHandlers.ofString(); + return client.send(request, handler); + + } catch (Exception exception) { + exception.printStackTrace(); + return null; + } + } + + /// Отправляет POST-запрос и возвращает ответ сервера + protected HttpResponse responseOfNewPostRequest(String stringUri, T object) { + URI uri = URI.create(host + stringUri); + + try (HttpClient client = HttpClient.newHttpClient()) { + String objectToJson = gson.toJson(object); + + HttpRequest request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(objectToJson)) + .uri(uri) + .build(); + HttpResponse.BodyHandler handler = HttpResponse.BodyHandlers.ofString(); + return client.send(request, handler); + + } catch (Exception exception) { + exception.printStackTrace(); + return null; + } + } + + /// Отправляет DELETE-запрос и возвращает ответ сервера + protected HttpResponse responseOfNewDeleteRequest(String stringUri) { + URI uri = URI.create(host + stringUri); + + try (HttpClient client = HttpClient.newHttpClient()) { + HttpRequest request = HttpRequest.newBuilder().DELETE().uri(uri).build(); + HttpResponse.BodyHandler handler = HttpResponse.BodyHandlers.ofString(); + return client.send(request, handler); + + } catch (Exception exception) { + exception.printStackTrace(); + return null; + } + } + +} diff --git a/test/server/handlers/EpicsHandlerTest.java b/test/server/handlers/EpicsHandlerTest.java new file mode 100644 index 0000000..ec557f0 --- /dev/null +++ b/test/server/handlers/EpicsHandlerTest.java @@ -0,0 +1,200 @@ +package server.handlers; + +import data.*; +import org.junit.jupiter.api.Test; +import server.HttpTaskServerTest; +import server.tokens.TaskListTypeToken; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class EpicsHandlerTest extends HttpTaskServerTest { + + @Test + void GET_EPIC() { + shouldReturnStatus200AndBodyOfEpic3AfterGetEpic3(); + shouldReturnStatus404AndBodyOfNotFoundAfterGetEpic1(); + } + + @Test + void GET_ALL_EPICS() { + shouldReturnStatus200AndListOfTwoElementsAfterGetAllEpics(); + } + + @Test + void POST_NEW_EPIC() { + shouldReturnStatus200AndBodyOfSuccessAddingAfterAddNewEpic(); + shouldReturnStatus400AndBodyOfBadRequestAfterTryAddBrokenEpic(); + } + + @Test + void POST_UPDATE_EPIC() { + shouldReturnStatus200AndBodyOfSuccessUpdatingAfterUpdateEpic(); + shouldReturnStatus422AndBodyOfUnsupportedAfterTryUpdateEpic(); + shouldReturnStatus404AndBodyOfNotFoundAfterTryUpdateLostEpic(); + + } + + @Test + void DELETE_EPIC() { + shouldReturnStatus200AndBodyOfSuccessRemovingAfterDeleteEpic(); + } + + @Test + void GET_ALL_SUBTASKS_BY_EPIC() { + shouldReturnStatus200AndListOfTwoElementsAfterGetAllSubtasksByEpic(); + } + + + // ---------------------------------HOOD---------------------------------------- + + + /// Создает новый эпик, запрашивает его и сверяет поля + private void shouldReturnStatus200AndBodyOfEpic3AfterGetEpic3() { + manager.addEpic(new Epic(null, "FIRST EPIC", "first epic")); + + HttpResponse response = responseOfNewGetRequest("epics/3"); + + assertEquals(200, response.statusCode()); + + Epic requestEpic = gson.fromJson(response.body(), Epic.class); + assertEquals("FIRST EPIC", requestEpic.getTitle()); + assertEquals("first epic", requestEpic.getDescription()); + assertEquals(Status.NEW, requestEpic.getStatus()); + } + + /// Запрашивает эпик, но в id указан id задачи + private void shouldReturnStatus404AndBodyOfNotFoundAfterGetEpic1() { + HttpResponse response = responseOfNewGetRequest("epics/1"); + + assertEquals(404, response.statusCode()); + assertEquals("\tGET_EPIC 404 not found", response.body()); + } + + /// Создает два эпика, запрашивает все эпики, проверяет код ответа и количество + private void shouldReturnStatus200AndListOfTwoElementsAfterGetAllEpics() { + manager.addEpic(new Epic(null, "FIRST EPIC", "first epic")); + manager.addEpic(new Epic(null, "SECOND EPIC", "second epic")); + + HttpResponse response = responseOfNewGetRequest("epics"); + + assertEquals(200, response.statusCode()); + + List requestList = gson.fromJson(response.body(), new TaskListTypeToken().getType()); + assertEquals(2, requestList.size()); + } + + /// Постит новый эпик, проверяет код и тело ответа + private void shouldReturnStatus200AndBodyOfSuccessAddingAfterAddNewEpic() { + Epic epic = new Epic(null, "ADDED EPIC", "added epic"); + + HttpResponse response = responseOfNewPostRequest("epics", epic); + + assertEquals(200, response.statusCode()); + assertEquals("epic adding success, epicId=3", response.body()); + } + + /// Создает новый эпик, постит обновление этого эпика, проверяет статус и тело ответа + private void shouldReturnStatus200AndBodyOfSuccessUpdatingAfterUpdateEpic() { + manager.addEpic(new Epic(null, "FIRST EPIC", "first epic")); + + Epic newEpic = new Epic(3, "FIRST EPIC UPDATED", null); + + HttpResponse response = responseOfNewPostRequest("epics/3", newEpic); + assertEquals(200, response.statusCode()); + + assertEquals("epic updated success", response.body()); + + Epic requestTask = (Epic) manager.getWithoutHistory(3); + assertEquals("FIRST EPIC UPDATED", requestTask.getTitle()); + assertEquals("first epic", requestTask.getDescription()); + + } + + /// Пытается обновить эпик, но в теле эпика не указан ID + private void shouldReturnStatus422AndBodyOfUnsupportedAfterTryUpdateEpic() { + Epic epic = new Epic(null, "TASK WITHOUT ID", null); + + HttpResponse response = responseOfNewPostRequest("epics/3", epic); + + assertEquals(422, response.statusCode()); + assertEquals("\t" + Endpoint.POST_UPDATE_EPIC + " 422 unprocessable entity", response.body()); + } + + /// Пытается обновить эпик с несуществующим id + private void shouldReturnStatus404AndBodyOfNotFoundAfterTryUpdateLostEpic() { + Epic epic = new Epic(4, "TASK WITH LOST ID", null); + + HttpResponse response = responseOfNewPostRequest("epics/4", epic); + + assertEquals(404, response.statusCode()); + assertEquals("\t" + Endpoint.POST_UPDATE_EPIC + " 404 not found", response.body()); + } + + /// Удаляет эпик и проверяет ответ от сервера + private void shouldReturnStatus200AndBodyOfSuccessRemovingAfterDeleteEpic() { + manager.addEpic(new Epic(null, "FIRST EPIC", "first epic")); + + HttpResponse response = responseOfNewDeleteRequest("epics/3"); + + assertEquals(200, response.statusCode()); + assertEquals("epic removed success", response.body()); + } + + /** + * Создает эпик с двумя подзадачами, запрашивает все подзадачи этого эпика, + * проверяет статус ответа и количество подзадач в ответе + */ + private void shouldReturnStatus200AndListOfTwoElementsAfterGetAllSubtasksByEpic() { + manager.addEpic(new Epic(null, "FIRST EPIC", "first epic")); + manager.addSubtask(new Subtask(null, "FIRST SUB", "first sub", + null, 3, null, null)); + manager.addSubtask(new Subtask(null, "SECOND SUB", "second sub", + null, 3, null, null)); + + HttpResponse response = responseOfNewGetRequest("epics/3/subtasks"); + + assertEquals(200, response.statusCode()); + + List requestList = gson.fromJson(response.body(), new TaskListTypeToken().getType()); + assertEquals(2, requestList.size()); + } + + /// Пытается добавить новый эпик, но отправляет сломанный json в теле запроса + private void shouldReturnStatus400AndBodyOfBadRequestAfterTryAddBrokenEpic() { + String brokenEpic = """ + { + "id" : "THIS IS FAIL", + "title" : "BROKEN EPIC", + "description" : "broken epic", + "status" : null, + "startTime" : null, + "duration" : 0 + }"""; + + URI uri = URI.create(host + "epics"); + + HttpResponse response; + try (HttpClient client = HttpClient.newHttpClient()) { + + HttpRequest request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(brokenEpic)) + .uri(uri) + .build(); + HttpResponse.BodyHandler handler = HttpResponse.BodyHandlers.ofString(); + response = client.send(request, handler); + + } catch (Exception exception) { + exception.printStackTrace(); + return; + } + + assertEquals(400, response.statusCode()); + assertEquals("\t" + Endpoint.POST_NEW_EPIC + " 400 bad request body", response.body()); + } +} diff --git a/test/server/handlers/HistoryHandlerTest.java b/test/server/handlers/HistoryHandlerTest.java new file mode 100644 index 0000000..1f44ea6 --- /dev/null +++ b/test/server/handlers/HistoryHandlerTest.java @@ -0,0 +1,28 @@ +package server.handlers; + +import data.Task; +import org.junit.jupiter.api.Test; +import server.HttpTaskServerTest; +import server.tokens.TaskListTypeToken; + +import java.net.http.HttpResponse; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class HistoryHandlerTest extends HttpTaskServerTest { + + @Test + void GET_HISTORY() { + shouldReturnStatus200AndBodyOfHistoryTasks(); + } + + /// Добавляет задачу в историю, запрашивает список истории и проверяет количество + private void shouldReturnStatus200AndBodyOfHistoryTasks() { + manager.getTask(1); + + HttpResponse response = responseOfNewGetRequest("history"); + List requestList = gson.fromJson(response.body(), new TaskListTypeToken().getType()); + assertEquals(1, requestList.size()); + } +} diff --git a/test/server/handlers/PrioritizedHandlerTest.java b/test/server/handlers/PrioritizedHandlerTest.java new file mode 100644 index 0000000..ed74f08 --- /dev/null +++ b/test/server/handlers/PrioritizedHandlerTest.java @@ -0,0 +1,38 @@ +package server.handlers; + +import data.Task; +import org.junit.jupiter.api.Test; +import server.HttpTaskServerTest; +import server.tokens.TaskListTypeToken; + +import java.net.http.HttpResponse; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PrioritizedHandlerTest extends HttpTaskServerTest { + + @Test + void GET_PRIORITIZED() { + shouldReturnStatus200AndBodyOfPrioritizedTasks(); + } + + /// Приоритизирует задачи, запрашивает их список и проверяет их количество + private void shouldReturnStatus200AndBodyOfPrioritizedTasks() { + manager.updateTask(new Task(1, null, null, null, + LocalDateTime.parse("2025-11-20 20:00", DATE_TIME_FORMATTER), + Duration.ofMinutes(30) + )); + + manager.updateTask(new Task(2, null, null, null, + LocalDateTime.parse("2025-11-20 20:30", DATE_TIME_FORMATTER), + Duration.ofMinutes(30) + )); + + HttpResponse response = responseOfNewGetRequest("prioritized"); + List requestList = gson.fromJson(response.body(), new TaskListTypeToken().getType()); + assertEquals(2, requestList.size()); + } +} diff --git a/test/server/handlers/SubtasksHandlerTest.java b/test/server/handlers/SubtasksHandlerTest.java new file mode 100644 index 0000000..2e0ca84 --- /dev/null +++ b/test/server/handlers/SubtasksHandlerTest.java @@ -0,0 +1,147 @@ +package server.handlers; + +import data.Endpoint; +import data.Epic; +import data.Subtask; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import server.HttpTaskServerTest; +import server.tokens.TaskListTypeToken; + +import java.net.http.HttpResponse; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SubtasksHandlerTest extends HttpTaskServerTest { + + @BeforeEach + void subtaskBeforeEach() { + manager.addEpic(new Epic(3, "FIRST EPIC", "first epic")); + manager.addSubtask(new Subtask(4, "FIRST SUB", "first sub", + null, 3, null, null)); + manager.addSubtask(new Subtask(5, "SECOND SUB", "second sub", + null, 3, null, null)); + } + + @Test + void GET_SUBTASK() { + shouldReturnStatus200AndBodyOfSubtask4AfterGetSubtask4(); + } + + @Test + void GET_ALL_SUBTASKS() { + shouldReturnStatus200AndListOfTwoElementsAfterGetAllSubtasks(); + } + + @Test + void POST_NEW_SUBTASK() { + shouldReturnStatus200AndBodyOfSuccessAddingAfterAddNewSubtask(); + shouldReturnStatus422AndBodyOfUnsupportedAfterTryAddSubtaskWithoutEpicId(); + } + + @Test + void POST_UPDATE_SUBTASK() { + shouldReturnStatus200AndBodyOfSuccessUpdatingAfterUpdateSubtask(); + shouldReturnStatus422AndBodyOfUnsupportedAfterTryUpdateSubtaskWithoutId(); + shouldReturnStatus404AndBodyOfNotFoundAfterTryUpdateLostSubtask(); + } + + @Test + void DELETE_SUBTASK() { + shouldReturnStatus200AndBodyOfSuccessRemovingAfterDeleteSubtask(); + } + + + // ---------------------------------HOOD---------------------------------------- + + + /// Получает задачу из базы и сверяет все поля с оригиналом + private void shouldReturnStatus200AndBodyOfSubtask4AfterGetSubtask4() { + HttpResponse response = responseOfNewGetRequest("subtasks/4"); + + assertEquals(200, response.statusCode()); + + Subtask requestSubtask = gson.fromJson(response.body(), Subtask.class); + assertEquals("FIRST SUB", requestSubtask.getTitle()); + assertEquals("first sub", requestSubtask.getDescription()); + } + + /// Получает список всех подзадач и проверяет их количество + private void shouldReturnStatus200AndListOfTwoElementsAfterGetAllSubtasks() { + HttpResponse response = responseOfNewGetRequest("subtasks"); + + assertEquals(200, response.statusCode()); + + List requestList = gson.fromJson(response.body(), new TaskListTypeToken().getType()); + assertEquals(2, requestList.size()); + } + + /// Добавляет новую подзадачу и проверяет ответ сервера + private void shouldReturnStatus200AndBodyOfSuccessAddingAfterAddNewSubtask() { + Subtask subtask = new Subtask(null, "ADDED SUB", "added SUB", + null, 3, null, null); + + HttpResponse response = responseOfNewPostRequest("subtasks", subtask); + + assertEquals(200, response.statusCode()); + assertEquals("subtask adding success, subtaskId=6", response.body()); + } + + /// Пытается добавить подзадачу, но в теле подзадачи не указан ID эпика + private void shouldReturnStatus422AndBodyOfUnsupportedAfterTryAddSubtaskWithoutEpicId() { + Subtask subtask = new Subtask(null, "TASK WITHOUT EPIC-ID", + null, null, null, null, null); + + HttpResponse response = responseOfNewPostRequest("subtasks", subtask); + + assertEquals(422, response.statusCode()); + assertEquals("\t" + Endpoint.POST_NEW_SUBTASK + " 422 unprocessable entity", response.body()); + } + + /// Обновляет существующую подзадачу и проверяет ответ от сервера + private void shouldReturnStatus200AndBodyOfSuccessUpdatingAfterUpdateSubtask() { + Subtask newSubtask = new Subtask(5, "SECOND SUB UPDATED", + null, null, null, null, null); + + HttpResponse response = responseOfNewPostRequest("subtasks/5", newSubtask); + assertEquals(200, response.statusCode()); + + assertEquals("subtask updated success", response.body()); + + Subtask requestTask = (Subtask) manager.getWithoutHistory(5); + assertEquals("SECOND SUB UPDATED", requestTask.getTitle()); + assertEquals("second sub", requestTask.getDescription()); + } + + /// Пытается обновить подзадачу, но в теле подзадачи не указан ID подзадачи + private void shouldReturnStatus422AndBodyOfUnsupportedAfterTryUpdateSubtaskWithoutId() { + Subtask subtask = new Subtask(null, "TASK WITHOUT ID", + null, null, 3, null, null); + + HttpResponse response = responseOfNewPostRequest("tasks/1", subtask); + + assertEquals(422, response.statusCode()); + assertEquals("\t" + Endpoint.POST_UPDATE_TASK + " 422 unprocessable entity", response.body()); + } + + /// Пытается обновить подзадачу с несуществующим id + private void shouldReturnStatus404AndBodyOfNotFoundAfterTryUpdateLostSubtask() { + Subtask subtask = new Subtask(7, "TASK WITH LOST ID", + null, null, null, null, null); + + HttpResponse response = responseOfNewPostRequest("subtasks/7", subtask); + + assertEquals(404, response.statusCode()); + assertEquals("\t" + Endpoint.POST_UPDATE_SUBTASK + " 404 not found", response.body()); + } + + /// Удаляет подзадачу и проверяет ответ от сервера + private void shouldReturnStatus200AndBodyOfSuccessRemovingAfterDeleteSubtask() { + HttpResponse response = responseOfNewDeleteRequest("subtasks/4"); + + assertEquals(200, response.statusCode()); + assertEquals("subtask removed success", response.body()); + } + +} diff --git a/test/server/handlers/TasksHandlerTest.java b/test/server/handlers/TasksHandlerTest.java new file mode 100644 index 0000000..0d4ace2 --- /dev/null +++ b/test/server/handlers/TasksHandlerTest.java @@ -0,0 +1,264 @@ +package server.handlers; + +import data.Endpoint; +import data.Status; +import data.Task; +import org.junit.jupiter.api.Test; +import server.HttpTaskServerTest; +import server.tokens.TaskListTypeToken; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TasksHandlerTest extends HttpTaskServerTest { + + @Test + void BASE() { + shouldReturnStatus405AndBodyOfFormatExceptionAfterGetTaskWithStringId(); + } + + @Test + void GET_TASK() { + shouldReturnStatus200AndBodyOfTask1AfterGetTask1(); + shouldReturnStatus404AndBodyOfNotFoundAfterGetTask3(); + shouldReturnStatus405AndBodyOfFormatExceptionAfterGetTaskWithLongId(); + shouldReturnStatus406AndBodyOfTimeOverlapsAfterTryAddNewIntersectionTask(); + } + + @Test + void GET_ALL_TASK() { + shouldReturnStatus200AndListOfTwoElementsAfterGetAllTasks(); + } + + @Test + void POST_NEW_TASK() { + shouldReturnStatus200AndBodyOfSuccessAddingAfterAddNewTask(); + shouldReturnStatus400AndBodyOfBadRequestAfterTryAddingBrokenTask(); + } + + @Test + void POST_UPDATE_TASK() { + shouldReturnStatus200AndBodyOfSuccessUpdatingAfterUpdateTask(); + shouldReturnStatus422AndBodyOfUnsupportedAfterTryUpdateTask(); + shouldReturnStatus404AndBodyOfNotFoundAfterTryUpdateLostTask(); + shouldReturnStatus422AndBodyOfUnsupportedAfterTryUpdateWrongTask(); + shouldReturnStatus406AndBodyOfOverlapsAfterTryUpdateIntersectionTask(); + shouldReturnStatus200AndBodyOfTask2AfterTryGetHistory(); + } + + @Test + void DELETE_TASK() { + shouldReturnStatus200AndBodyOfSuccessRemovingAfterDeleteTask(); + } + + + // ---------------------------------HOOD---------------------------------------- + + + /// Пытается получить задачу, но вместо id указана буква + private void shouldReturnStatus405AndBodyOfFormatExceptionAfterGetTaskWithStringId() { + HttpResponse response = responseOfNewGetRequest("tasks/l"); + + assertEquals(405, response.statusCode()); + assertEquals("\tUNKNOWN 405 path format exception: /tasks/l", response.body()); + } + + /// Пытается получить задачу, но указан слишком большой id + private void shouldReturnStatus405AndBodyOfFormatExceptionAfterGetTaskWithLongId() { + HttpResponse response = responseOfNewGetRequest("tasks/9999999999"); + + assertEquals(405, response.statusCode()); + assertEquals("\tGET_TASK 405 path format exception: /tasks/9999999999", response.body()); + } + + /// Пытается получить задачу, но задачи с таким id нет в базе + private void shouldReturnStatus404AndBodyOfNotFoundAfterGetTask3() { + HttpResponse response = responseOfNewGetRequest("tasks/3"); + + assertEquals(404, response.statusCode()); + assertEquals("\tGET_TASK 404 not found", response.body()); + } + + /// Пытается добавить задачу, но она пересекается по срокам с существующей + private void shouldReturnStatus406AndBodyOfTimeOverlapsAfterTryAddNewIntersectionTask() { + manager.updateTask(new Task(2, null, null, null, + LocalDateTime.parse("2025-11-20 08:00", DATE_TIME_FORMATTER), + Duration.ofMinutes(30) + )); + + Task newTask = new Task(null, "NEW TASK", "new task", null, + LocalDateTime.parse("2025-11-20 08:20", DATE_TIME_FORMATTER), + Duration.ofMinutes(30) + ); + + HttpResponse response = responseOfNewPostRequest("tasks", newTask); + + assertEquals(406, response.statusCode()); + assertEquals("\tPOST_NEW_TASK 406 time overlaps: 2025-11-20 08:20 - 2025-11-20 08:50", + response.body() + ); + } + + /// Получает задачу из базы и сверяет все поля с оригиналом + private void shouldReturnStatus200AndBodyOfTask1AfterGetTask1() { + HttpResponse response = responseOfNewGetRequest("tasks/1"); + + assertEquals(200, response.statusCode()); + + Task requestTask = gson.fromJson(response.body(), Task.class); + assertEquals(task1.getTitle(), requestTask.getTitle()); + assertEquals(task1.getDescription(), requestTask.getDescription()); + assertEquals(task1.getStatus(), requestTask.getStatus()); + assertEquals(task1.getStartTime(), requestTask.getStartTime()); + assertEquals(task1.getDuration(), requestTask.getDuration()); + } + + /// Получает список всех задач и проверяет их количество + private void shouldReturnStatus200AndListOfTwoElementsAfterGetAllTasks() { + HttpResponse response = responseOfNewGetRequest("tasks"); + + assertEquals(200, response.statusCode()); + + List requestList = gson.fromJson(response.body(), new TaskListTypeToken().getType()); + assertEquals(2, requestList.size()); + } + + /// Добавляет новую задачу и проверяет ответ сервера + private void shouldReturnStatus200AndBodyOfSuccessAddingAfterAddNewTask() { + Task task3 = new Task(null, "ADDED TASK", "added task"); + + HttpResponse response = responseOfNewPostRequest("tasks", task3); + + assertEquals(200, response.statusCode()); + assertEquals("task adding success, taskId=3", response.body()); + } + + /// Пытается добавить новую задачу через неправильно заполненный json + private void shouldReturnStatus400AndBodyOfBadRequestAfterTryAddingBrokenTask() { + String brokenString = """ + { + "id": null, + "title": "BROKEN TASK", + "description": "broken task", + "status": null, + "startTime": "THIS IS FAIL", + "duration": 32 + }"""; + + URI uri = URI.create(host + "tasks"); + + HttpResponse response; + try (HttpClient client = HttpClient.newHttpClient()) { + + HttpRequest request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(brokenString)) + .uri(uri) + .build(); + HttpResponse.BodyHandler handler = HttpResponse.BodyHandlers.ofString(); + response = client.send(request, handler); + + } catch (Exception exception) { + exception.printStackTrace(); + return; + } + + assertEquals(400, response.statusCode()); + assertEquals("\t" + Endpoint.POST_NEW_TASK + " 400 bad request body", response.body()); + } + + /** + * Обновляет существующую задачу и проверяет ответ от сервера, + * затем получает ее через менеджер и сверяет все поля + */ + private void shouldReturnStatus200AndBodyOfSuccessUpdatingAfterUpdateTask() { + Task task3 = new Task( + 2, + "UPDATED TASK", + null, + Status.IN_PROGRESS, + LocalDateTime.parse("2025-11-20 10:00", DATE_TIME_FORMATTER), + Duration.ofMinutes(15) + ); + + HttpResponse response = responseOfNewPostRequest("tasks/2", task3); + assertEquals(200, response.statusCode()); + + assertEquals("task updated success", response.body()); + + Task requestTask = manager.getWithoutHistory(2); + assertEquals("UPDATED TASK", requestTask.getTitle()); + assertEquals("second task", requestTask.getDescription()); + assertEquals(Status.IN_PROGRESS, requestTask.getStatus()); + assertEquals(LocalDateTime.parse("2025-11-20 10:00", DATE_TIME_FORMATTER), requestTask.getStartTime()); + assertEquals(LocalDateTime.parse("2025-11-20 10:15", DATE_TIME_FORMATTER), requestTask.getEndTime()); + } + + /// Пытается обновить задачу, но в теле задачи не указан ID + private void shouldReturnStatus422AndBodyOfUnsupportedAfterTryUpdateTask() { + Task task = new Task(null, "TASK WITHOUT ID", null); + + HttpResponse response = responseOfNewPostRequest("tasks/1", task); + + assertEquals(422, response.statusCode()); + assertEquals("\t" + Endpoint.POST_UPDATE_TASK + " 422 unprocessable entity", response.body()); + } + + /// Пытается обновить задачу с несуществующим id + private void shouldReturnStatus404AndBodyOfNotFoundAfterTryUpdateLostTask() { + Task task = new Task(3, "TASK WITH LOST ID", null); + + HttpResponse response = responseOfNewPostRequest("tasks/3", task); + + assertEquals(404, response.statusCode()); + assertEquals("\t" + Endpoint.POST_UPDATE_TASK + " 404 not found", response.body()); + } + + /// Пытается обновить задачу, но в эндпоинте и теле указаны разные существующие id + private void shouldReturnStatus422AndBodyOfUnsupportedAfterTryUpdateWrongTask() { + Task task = new Task(2, "TASK WITH LOST ID", null); + + HttpResponse response = responseOfNewPostRequest("tasks/1", task); + + assertEquals(422, response.statusCode()); + assertEquals("\t" + Endpoint.POST_UPDATE_TASK + " 422 unprocessable entity", response.body()); + } + + /// Пытается обновить задачу, но время пересекается с уже существующей + private void shouldReturnStatus406AndBodyOfOverlapsAfterTryUpdateIntersectionTask() { + Task task = new Task( + 1, + "UPDATED TASK", + null, + Status.IN_PROGRESS, + LocalDateTime.parse("2025-11-20 10:10", DATE_TIME_FORMATTER), + Duration.ofMinutes(10) + ); + + HttpResponse response = responseOfNewPostRequest("tasks/1", task); + assertEquals(406, response.statusCode()); + + assertEquals("\tPOST_UPDATE_TASK 406 time overlaps: 2025-11-20 10:10 - 2025-11-20 10:20", + response.body()); + } + + /// Проверяет историю после множества действий по обновлению задач + private void shouldReturnStatus200AndBodyOfTask2AfterTryGetHistory() { + HttpResponse response = responseOfNewGetRequest("history"); + assertEquals(404, response.statusCode()); + } + + /// Удаляет задачу и проверяет ответ от сервера + private void shouldReturnStatus200AndBodyOfSuccessRemovingAfterDeleteTask() { + HttpResponse response = responseOfNewDeleteRequest("tasks/1"); + + assertEquals(200, response.statusCode()); + assertEquals("task removed success", response.body()); + } +} diff --git a/test/server/tokens/TaskListTypeToken.java b/test/server/tokens/TaskListTypeToken.java new file mode 100644 index 0000000..fee7e12 --- /dev/null +++ b/test/server/tokens/TaskListTypeToken.java @@ -0,0 +1,9 @@ +package server.tokens; + +import com.google.gson.reflect.TypeToken; +import data.Task; + +import java.util.List; + +public class TaskListTypeToken extends TypeToken> { +}