From d3b5760572abd65f19d4c48147f8d476e8fc99e7 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 18 Sep 2025 23:29:37 +0300 Subject: [PATCH 01/22] refactor: enums package --- src/managers/InMemoryTaskManager.java | 2 +- src/managers/filedbacked/FileBackedTaskManager.java | 8 ++++---- src/managers/filedbacked/ParserHelper.java | 4 ++-- src/model/Epic.java | 4 ++-- src/model/SubTask.java | 4 ++-- src/model/Task.java | 4 ++-- src/util/TaskTimeController.java | 1 + src/util/{ => enums}/CsvField.java | 2 +- src/util/{ => enums}/Status.java | 2 +- test/managers/FileBackedTaskManagerTest.java | 2 +- test/managers/InMemoryHistoryManagerTest.java | 2 +- test/managers/InMemoryTaskManagerTest.java | 2 +- test/model/EpicTest.java | 2 +- test/model/SubTaskTest.java | 2 +- test/model/TaskTest.java | 4 ++-- test/util/TaskTimeControllerTest.java | 2 ++ 16 files changed, 25 insertions(+), 22 deletions(-) rename src/util/{ => enums}/CsvField.java (93%) rename src/util/{ => enums}/Status.java (74%) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index 56537ee..70a8e2f 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -4,7 +4,7 @@ import model.Epic; import model.SubTask; import model.Task; -import util.Status; +import util.enums.Status; import util.TaskTimeController; import util.exceptions.TaskTimeOverlapException; diff --git a/src/managers/filedbacked/FileBackedTaskManager.java b/src/managers/filedbacked/FileBackedTaskManager.java index 7174fcc..5a87fb8 100644 --- a/src/managers/filedbacked/FileBackedTaskManager.java +++ b/src/managers/filedbacked/FileBackedTaskManager.java @@ -4,8 +4,8 @@ import model.Epic; import model.SubTask; import model.Task; -import util.Status; -import util.Type; +import util.enums.Status; +import util.enums.Type; import util.exceptions.ManagerLoadException; import util.exceptions.ManagerSaveException; @@ -17,8 +17,8 @@ import java.util.List; import static managers.filedbacked.ParserHelper.*; -import static util.CsvField.*; -import static util.Type.*; +import static util.enums.CsvField.*; +import static util.enums.Type.*; /** * Менеджер задач с сохранением состояния в файл типа csv. diff --git a/src/managers/filedbacked/ParserHelper.java b/src/managers/filedbacked/ParserHelper.java index 6a80e9e..6823009 100644 --- a/src/managers/filedbacked/ParserHelper.java +++ b/src/managers/filedbacked/ParserHelper.java @@ -1,7 +1,7 @@ package managers.filedbacked; -import util.Status; -import util.Type; +import util.enums.Status; +import util.enums.Type; import util.exceptions.ManagerLoadException; import java.time.Duration; diff --git a/src/model/Epic.java b/src/model/Epic.java index f451675..5a8a095 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -1,7 +1,7 @@ package model; -import util.Status; -import util.Type; +import util.enums.Status; +import util.enums.Type; import java.time.Duration; import java.time.LocalDateTime; diff --git a/src/model/SubTask.java b/src/model/SubTask.java index 439a05f..5e406a7 100644 --- a/src/model/SubTask.java +++ b/src/model/SubTask.java @@ -1,7 +1,7 @@ package model; -import util.Status; -import util.Type; +import util.enums.Status; +import util.enums.Type; import java.time.LocalDateTime; diff --git a/src/model/Task.java b/src/model/Task.java index 7e31d1f..4bd866a 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -1,7 +1,7 @@ package model; -import util.Status; -import util.Type; +import util.enums.Status; +import util.enums.Type; import java.time.Duration; import java.time.LocalDateTime; diff --git a/src/util/TaskTimeController.java b/src/util/TaskTimeController.java index 4194747..4643860 100644 --- a/src/util/TaskTimeController.java +++ b/src/util/TaskTimeController.java @@ -3,6 +3,7 @@ import model.Epic; import model.SubTask; import model.Task; +import util.enums.Type; import java.time.LocalDateTime; import java.util.Comparator; diff --git a/src/util/CsvField.java b/src/util/enums/CsvField.java similarity index 93% rename from src/util/CsvField.java rename to src/util/enums/CsvField.java index 3f3b79f..31370d2 100644 --- a/src/util/CsvField.java +++ b/src/util/enums/CsvField.java @@ -1,4 +1,4 @@ -package util; +package util.enums; public enum CsvField { ID(0), diff --git a/src/util/Status.java b/src/util/enums/Status.java similarity index 74% rename from src/util/Status.java rename to src/util/enums/Status.java index 9e4a21d..1b22897 100644 --- a/src/util/Status.java +++ b/src/util/enums/Status.java @@ -1,4 +1,4 @@ -package util; +package util.enums; public enum Status { NEW, diff --git a/test/managers/FileBackedTaskManagerTest.java b/test/managers/FileBackedTaskManagerTest.java index eb4a9c7..0d2d48f 100644 --- a/test/managers/FileBackedTaskManagerTest.java +++ b/test/managers/FileBackedTaskManagerTest.java @@ -6,7 +6,7 @@ import model.Task; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import util.Status; +import util.enums.Status; import util.exceptions.ManagerLoadException; import util.exceptions.ManagerSaveException; diff --git a/test/managers/InMemoryHistoryManagerTest.java b/test/managers/InMemoryHistoryManagerTest.java index eb1f007..c4e8c6d 100644 --- a/test/managers/InMemoryHistoryManagerTest.java +++ b/test/managers/InMemoryHistoryManagerTest.java @@ -5,7 +5,7 @@ import model.SubTask; import model.Task; import org.junit.jupiter.api.Test; -import util.Status; +import util.enums.Status; import static org.junit.jupiter.api.Assertions.*; diff --git a/test/managers/InMemoryTaskManagerTest.java b/test/managers/InMemoryTaskManagerTest.java index 174f55f..8aa53cb 100644 --- a/test/managers/InMemoryTaskManagerTest.java +++ b/test/managers/InMemoryTaskManagerTest.java @@ -5,7 +5,7 @@ import model.Task; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import util.Status; +import util.enums.Status; import util.exceptions.TaskTimeOverlapException; import java.time.LocalDateTime; diff --git a/test/model/EpicTest.java b/test/model/EpicTest.java index 4dc9a59..eaa2377 100644 --- a/test/model/EpicTest.java +++ b/test/model/EpicTest.java @@ -1,7 +1,7 @@ package model; import org.junit.jupiter.api.Test; -import util.Status; +import util.enums.Status; import java.time.Duration; import java.time.LocalDateTime; diff --git a/test/model/SubTaskTest.java b/test/model/SubTaskTest.java index dce8d68..23b0160 100644 --- a/test/model/SubTaskTest.java +++ b/test/model/SubTaskTest.java @@ -2,7 +2,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import util.Status; +import util.enums.Status; public class SubTaskTest { diff --git a/test/model/TaskTest.java b/test/model/TaskTest.java index d0b0d98..5e2794a 100644 --- a/test/model/TaskTest.java +++ b/test/model/TaskTest.java @@ -1,8 +1,8 @@ package model; import org.junit.jupiter.api.Test; -import util.Status; -import util.Type; +import util.enums.Status; +import util.enums.Type; import java.time.Duration; import java.time.LocalDateTime; diff --git a/test/util/TaskTimeControllerTest.java b/test/util/TaskTimeControllerTest.java index b8e7a3b..74ce7ba 100644 --- a/test/util/TaskTimeControllerTest.java +++ b/test/util/TaskTimeControllerTest.java @@ -6,6 +6,8 @@ import model.SubTask; import model.Task; import org.junit.jupiter.api.Test; +import util.enums.Status; +import util.enums.Type; import java.time.Duration; import java.time.LocalDateTime; From dba8be34a875b861e453fe1471e7aff3e0e36934 Mon Sep 17 00:00:00 2001 From: Crodi Date: Tue, 23 Sep 2025 21:16:49 +0300 Subject: [PATCH 02/22] fix: negative duration fix --- src/model/Task.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/model/Task.java b/src/model/Task.java index 4bd866a..0aca7ca 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -26,7 +26,7 @@ public Task(String title, String description, Status status, long durationInMinu this.title = title; this.description = description; this.status = status; - this.duration = durationInMinutes < 0 ? null : Duration.ofMinutes(durationInMinutes); + this.duration = durationInMinutes <= 0 ? null : Duration.ofMinutes(durationInMinutes); this.startTime = startTime; } @@ -34,7 +34,7 @@ public Task(String title, String description, Status status, long durationInMinu this.title = title; this.description = description; this.status = status; - this.duration = Duration.ofMinutes(durationInMinutes); + this.duration = durationInMinutes <= 0 ? null : Duration.ofMinutes(durationInMinutes); } public int getTaskId() { From 856ac73d06aa1f26c145d7b0616d0a9348f25b1b Mon Sep 17 00:00:00 2001 From: Crodi Date: Tue, 23 Sep 2025 21:18:51 +0300 Subject: [PATCH 03/22] feat: add new methods --- src/managers/TaskManager.java | 8 ++++++++ src/managers/filedbacked/FileBackedTaskManager.java | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/src/managers/TaskManager.java b/src/managers/TaskManager.java index 8450f89..fc81db6 100644 --- a/src/managers/TaskManager.java +++ b/src/managers/TaskManager.java @@ -24,6 +24,12 @@ public interface TaskManager { SubTask getSubTask(int id); + Task getTaskWithoutHistory(int id); + + Epic getEpicWithoutHistory(int id); + + SubTask getSubTaskWithoutHistory(int id); + List getTasks(); List getEpics(); @@ -52,5 +58,7 @@ public interface TaskManager { void clearSubTasks(); + void clearSubTasksFromEpic(int id); + List getHistory(); } diff --git a/src/managers/filedbacked/FileBackedTaskManager.java b/src/managers/filedbacked/FileBackedTaskManager.java index 5a87fb8..8036425 100644 --- a/src/managers/filedbacked/FileBackedTaskManager.java +++ b/src/managers/filedbacked/FileBackedTaskManager.java @@ -277,4 +277,10 @@ public void clearSubTasks() { super.clearSubTasks(); save(); } + + @Override + public void clearSubTasksFromEpic(int id) { + super.clearSubTasks(); + save(); + } } From 7a9fd1481ebded67f7dbe99f7f1d0ab2fc1f8a0d Mon Sep 17 00:00:00 2001 From: Crodi Date: Tue, 23 Sep 2025 21:23:48 +0300 Subject: [PATCH 04/22] feat: add clearSubTasksFromEpic, get*WithoutHistory refactor: TaskTimeOverlapException messages fix: update* , add exceptions, made updates safer, add update for new fields --- src/managers/InMemoryTaskManager.java | 72 +++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index 70a8e2f..edfbbfd 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -4,8 +4,9 @@ import model.Epic; import model.SubTask; import model.Task; -import util.enums.Status; import util.TaskTimeController; +import util.enums.Status; +import util.exceptions.TaskNotFound; import util.exceptions.TaskTimeOverlapException; import java.util.ArrayList; @@ -43,7 +44,7 @@ public class InMemoryTaskManager implements TaskManager { @Override public void addTask(Task task) throws TaskTimeOverlapException { if (taskTimeController.isTimeOverlapping(task)) { - throw new TaskTimeOverlapException("OVERLAP! Can`t add task: " + task); + throw new TaskTimeOverlapException("Time overlap! Can`t add task: " + task); } task.setTaskId(++idCount); tasks.put(task.getTaskId(), task); @@ -59,7 +60,7 @@ public void addEpic(Epic epic) { @Override public void addSubTask(SubTask subTask) throws TaskTimeOverlapException { if (taskTimeController.isTimeOverlapping(subTask)) { - throw new TaskTimeOverlapException("OVERLAP! Can`t add task: " + subTask); + throw new TaskTimeOverlapException("Time overlap! Can`t add subtask: " + subTask); } subTask.setTaskId(++idCount); subtasks.put(subTask.getTaskId(), subTask); @@ -90,6 +91,21 @@ public SubTask getSubTask(int id) { return subtasks.get(id); } + @Override + public Task getTaskWithoutHistory(int id) { + return tasks.get(id); + } + + @Override + public Epic getEpicWithoutHistory(int id) { + return epics.get(id); + } + + @Override + public SubTask getSubTaskWithoutHistory(int id) { + return subtasks.get(id); + } + @Override public List getTasks() { return List.copyOf(tasks.values()); @@ -124,12 +140,32 @@ public List getPrioritizedTasks() { @Override public void updateTask(Task task) { - tasks.put(task.getTaskId(), task); + int id = task.getTaskId(); + if (!tasks.containsKey(id)) { + throw new TaskNotFound("Task with id: " + id + " not found"); + } + if (taskTimeController.isTimeOverlapping(task)) { + throw new TaskTimeOverlapException("Time overlap! Can`t add task: " + task); + } + + Task oldTask = tasks.get(id); + taskTimeController.remove(oldTask); + + oldTask.setTitle(task.getTitle()); + oldTask.setDescription(task.getDescription()); + oldTask.setStatus(task.getStatus()); + oldTask.setDuration(task.getDuration()); + oldTask.setStartTime(task.getStartTime()); + + taskTimeController.add(task); } @Override public void updateEpic(Epic epic) { int id = epic.getTaskId(); + if (!epics.containsKey(id)) { + throw new TaskNotFound("Epic with id: " + id + " not found"); + } Epic oldEpic = epics.get(id); oldEpic.setTitle(epic.getTitle()); @@ -139,13 +175,23 @@ public void updateEpic(Epic epic) { @Override public void updateSubTask(SubTask subTask) { int id = subTask.getTaskId(); + if (!subtasks.containsKey(id)) { + throw new TaskNotFound("SubTask with id: " + id + " not found"); + } + if (taskTimeController.isTimeOverlapping(subTask)) { + throw new TaskTimeOverlapException("Time overlap! Can`t add subtask: " + subTask); + } SubTask oldSubTask = subtasks.get(id); + taskTimeController.remove(oldSubTask); oldSubTask.setTitle(subTask.getTitle()); oldSubTask.setDescription(subTask.getDescription()); oldSubTask.setStatus(subTask.getStatus()); + oldSubTask.setDuration(subTask.getDuration()); + oldSubTask.setStartTime(subTask.getStartTime()); - updateEpicStatus(epics.get(oldSubTask.getEpicId())); //обновляем статус эпика + taskTimeController.add(oldSubTask); + updateEpicStatus(epics.get(oldSubTask.getEpicId())); } /** @@ -304,6 +350,22 @@ public void clearSubTasks() { subtasks.clear(); } + @Override + public void clearSubTasksFromEpic(int id) { + if (!epics.containsKey(id)) { + throw new TaskNotFound("Epic with id: '" + id + "' not found"); + } + Epic epic = epics.get(id); + List subtasksFromEpic = epic.getSubtaskIds(); + + for (Integer key : subtasksFromEpic) { + taskTimeController.remove(subtasks.get(key)); + historyManager.remove(key); + subtasks.remove(key); + } + epic.clearSubtasks(); + } + public int getIdCount() { return idCount; } From e36da56ff78632bb2a1682522bc71f1345e57277 Mon Sep 17 00:00:00 2001 From: Crodi Date: Tue, 23 Sep 2025 21:24:26 +0300 Subject: [PATCH 05/22] feat: exception TaskNotFound --- src/util/exceptions/TaskNotFound.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/util/exceptions/TaskNotFound.java diff --git a/src/util/exceptions/TaskNotFound.java b/src/util/exceptions/TaskNotFound.java new file mode 100644 index 0000000..59e0307 --- /dev/null +++ b/src/util/exceptions/TaskNotFound.java @@ -0,0 +1,16 @@ +package util.exceptions; + +public class TaskNotFound extends RuntimeException { + + public TaskNotFound() { + super(); + } + + public TaskNotFound(String message) { + super(message); + } + + public TaskNotFound(String message, Throwable cause) { + super(message, cause); + } +} From a509c7755f3a2ac138c2b7a923db75150d057e49 Mon Sep 17 00:00:00 2001 From: Crodi Date: Tue, 23 Sep 2025 21:24:58 +0300 Subject: [PATCH 06/22] feat: enum for http endpoints refactor: enum Type moved to enums package --- src/util/enums/Endpoint.java | 13 +++++++++++++ src/util/{ => enums}/Type.java | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/util/enums/Endpoint.java rename src/util/{ => enums}/Type.java (72%) diff --git a/src/util/enums/Endpoint.java b/src/util/enums/Endpoint.java new file mode 100644 index 0000000..4dda49c --- /dev/null +++ b/src/util/enums/Endpoint.java @@ -0,0 +1,13 @@ +package util.enums; + +public enum Endpoint { + INVALID_SUBRESOURCE, + + INVALID, + INVALID_METHOD, + + OPTIONS, + GET, + POST, + DELETE +} diff --git a/src/util/Type.java b/src/util/enums/Type.java similarity index 72% rename from src/util/Type.java rename to src/util/enums/Type.java index dff2b72..9259e98 100644 --- a/src/util/Type.java +++ b/src/util/enums/Type.java @@ -1,4 +1,4 @@ -package util; +package util.enums; public enum Type { TASK, From c8df89656d41f83a31c09a706353f5fb828a2be3 Mon Sep 17 00:00:00 2001 From: Crodi Date: Tue, 23 Sep 2025 21:26:15 +0300 Subject: [PATCH 07/22] feat: RequestSegments - helper class to parse htpp path --- src/util/http/RequestSegments.java | 90 ++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/util/http/RequestSegments.java diff --git a/src/util/http/RequestSegments.java b/src/util/http/RequestSegments.java new file mode 100644 index 0000000..6d6a7d2 --- /dev/null +++ b/src/util/http/RequestSegments.java @@ -0,0 +1,90 @@ +package util.http; + +import com.sun.net.httpserver.HttpExchange; +import util.enums.Endpoint; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; + +public record RequestSegments( + Endpoint endpoint, + String resource, + int id, + Optional subResource) { + + private final static HashSet validMethods = new HashSet<>(List.of( + "GET", + "POST", + "DELETE", + "HEAD", + "OPTIONS") + ); + + public static RequestSegments getRequestSegments(HttpExchange exchange) { + String path = exchange.getRequestURI().getPath(); + return RequestSegments.parse(exchange.getRequestMethod(), path); + } + + private static RequestSegments parse(String method, String path) { + String[] parts = path.split("/"); + + return switch (parts.length) { + case 2 -> new RequestSegments( + parseEndpoint(method), + parts[1], + 0, + Optional.empty()); + case 3 -> new RequestSegments( + parseEndpoint(method), + parts[1], + parseInt(parts[2]), + Optional.empty()); + case 4 -> new RequestSegments( + parseEndpoint(method), + parts[1], + parseInt(parts[2]), + Optional.of(parts[3])); + default -> new RequestSegments( + Endpoint.INVALID_SUBRESOURCE, + parts[1], + 0, + Optional.empty()); + }; + } + + private static int parseInt(String idString) { + try { + int id = Integer.parseInt(idString); + if (id < 0) { + throw new NumberFormatException(); + } else { + return id; + } + } catch (NumberFormatException e) { + return -1; + } + } + + private static Endpoint parseEndpoint(String method) { + if (!validMethods.contains(method)) { + return Endpoint.INVALID_METHOD; + } + + Endpoint endpoint; + + if (method.equals("HEAD")) { + method = "GET"; + } + + try { + endpoint = Endpoint.valueOf(method); + } catch (IllegalArgumentException e) { + endpoint = Endpoint.INVALID; + } + + return endpoint; + } + + +} From 85d5a34f2af12b9654d4e0d70a2735065916679f Mon Sep 17 00:00:00 2001 From: Crodi Date: Tue, 23 Sep 2025 21:27:03 +0300 Subject: [PATCH 08/22] feat: ErrorResponse - helper class to send error json back to client JsonBuilder - helper class to construct json --- src/util/http/ErrorResponse.java | 17 ++++++++ src/util/http/JsonBuilder.java | 67 ++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/util/http/ErrorResponse.java create mode 100644 src/util/http/JsonBuilder.java diff --git a/src/util/http/ErrorResponse.java b/src/util/http/ErrorResponse.java new file mode 100644 index 0000000..72d914f --- /dev/null +++ b/src/util/http/ErrorResponse.java @@ -0,0 +1,17 @@ +package util.http; + +import com.google.gson.Gson; + +public class ErrorResponse { + private final String error; + private final String message; + + private ErrorResponse(String error, String message) { + this.error = error; + this.message = message; + } + + public static String ErrorToJson(Gson gson, String error, String message) { + return gson.toJson(new ErrorResponse(error, message)); + } +} \ No newline at end of file diff --git a/src/util/http/JsonBuilder.java b/src/util/http/JsonBuilder.java new file mode 100644 index 0000000..ae60c08 --- /dev/null +++ b/src/util/http/JsonBuilder.java @@ -0,0 +1,67 @@ +package util.http; + +import com.google.gson.Gson; + +import java.util.HashMap; +import java.util.Map; + +public class JsonBuilder { + private final Gson gson; + private RequestSegments segments; + + public JsonBuilder(Gson gson) { + this.gson = gson; + } + + public void setSegments(RequestSegments segments) { + this.segments = segments; + } + + public String message(String message) { + Map map = new HashMap<>(); + map.put("message", message); + return gson.toJson(map); + } + + public String tooMuchSubResources() { + return notFound("Subresources do not exist"); + } + + public String badRequest(String message) { + return ErrorResponse.ErrorToJson( + gson, + "Bad Request", + message + ); + } + + public String notFound(String message) { + return ErrorResponse.ErrorToJson( + gson, + "Not Found", + message + ); + } + + public String resourceNotFound() { + return notFound("Resource '" + segments.resource() + "' does not exist"); + } + + public String subresourceNotFound() { + return notFound("Subresource '" + segments.subResource().orElse("") + "' does not exist"); + } + + public String hasOverlaps() { + return ErrorResponse.ErrorToJson(gson, "Task time is overlapping", + "This task cannot be added due to overlap" + ); + } + + public String invalidId() { + return ErrorResponse.ErrorToJson( + gson, + "Bad Request", + "Id must be a positive integer" + ); + } +} From 320d3e9fdc85a7a3e6cc76356b5608dbf3282b35 Mon Sep 17 00:00:00 2001 From: Crodi Date: Tue, 23 Sep 2025 21:27:48 +0300 Subject: [PATCH 09/22] feat: gson adapters for duration and localdatetime --- src/util/gsonadapters/DurationAdapter.java | 22 ++++++++++ .../gsonadapters/LocalDateTimeAdapter.java | 40 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/util/gsonadapters/DurationAdapter.java create mode 100644 src/util/gsonadapters/LocalDateTimeAdapter.java diff --git a/src/util/gsonadapters/DurationAdapter.java b/src/util/gsonadapters/DurationAdapter.java new file mode 100644 index 0000000..9bb25bd --- /dev/null +++ b/src/util/gsonadapters/DurationAdapter.java @@ -0,0 +1,22 @@ +package util.gsonadapters; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.time.Duration; + +public class DurationAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter jsonWriter, Duration duration) throws IOException { + jsonWriter.value(duration == null ? null : duration.toMinutes()); + } + + @Override + public Duration read(JsonReader jsonReader) throws IOException { + String string = jsonReader.nextString(); + return Duration.ofMinutes(Long.parseLong(string)); + } +} diff --git a/src/util/gsonadapters/LocalDateTimeAdapter.java b/src/util/gsonadapters/LocalDateTimeAdapter.java new file mode 100644 index 0000000..40d269b --- /dev/null +++ b/src/util/gsonadapters/LocalDateTimeAdapter.java @@ -0,0 +1,40 @@ +package util.gsonadapters; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; + +public class LocalDateTimeAdapter extends TypeAdapter { + private static final List formats = + List.of(DateTimeFormatter.ISO_LOCAL_DATE_TIME, + DateTimeFormatter.ISO_DATE_TIME, + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"), + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"), + DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss") + ); + + @Override + public void write(JsonWriter jsonWriter, LocalDateTime localDateTime) throws IOException { + jsonWriter.value(localDateTime == null ? null : localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + } + + @Override + public LocalDateTime read(JsonReader jsonReader) throws IOException { + String date = jsonReader.nextString(); + for (DateTimeFormatter format : formats) { + try { + return LocalDateTime.parse(date, format); + } catch (DateTimeParseException e) { + continue; + } + } + + throw new DateTimeParseException("Cannot parse date: " + date, date, 0); + } +} From a049c543ac37b0a4efc2ec6f10b63ee7e946b325 Mon Sep 17 00:00:00 2001 From: Crodi Date: Tue, 23 Sep 2025 21:29:33 +0300 Subject: [PATCH 10/22] feat: HttpTaskServer - main http server --- src/http/HttpTaskServer.java | 78 ++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/http/HttpTaskServer.java diff --git a/src/http/HttpTaskServer.java b/src/http/HttpTaskServer.java new file mode 100644 index 0000000..3e8321f --- /dev/null +++ b/src/http/HttpTaskServer.java @@ -0,0 +1,78 @@ +package http; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.sun.net.httpserver.HttpServer; +import http.handlers.*; +import managers.TaskManager; +import managers.filedbacked.FileBackedTaskManager; +import util.gsonadapters.DurationAdapter; +import util.gsonadapters.LocalDateTimeAdapter; +import util.http.JsonBuilder; + +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Scanner; + +public class HttpTaskServer { + private final int PORT = 8080; + private HttpServer httpServer; + private final TaskManager manager; + + private final Gson gson; + private final JsonBuilder jsonBuilder; + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + String cmd = ""; + HttpTaskServer server = new HttpTaskServer(new FileBackedTaskManager(new File("resources/httpTasks.csv"))); + server.start(); + + System.out.println("Commands to stop: c, stop"); + while (!cmd.equals("c") && !cmd.equals("stop")) { + cmd = scanner.next(); + } + server.stop(); + } + + public HttpTaskServer(TaskManager manager) { + this.manager = manager; + this.gson = new GsonBuilder() + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter()) + .registerTypeAdapter(Duration.class, new DurationAdapter()) + .create(); + this.jsonBuilder = new JsonBuilder(gson); + createServer(); + } + + private void createServer() { + try { + httpServer = HttpServer.create(new InetSocketAddress(PORT), 0); + httpServer.createContext("/tasks", new TaskHandler(manager, gson, jsonBuilder)); + httpServer.createContext("/epics", new EpicsHandler(manager, gson, jsonBuilder)); + httpServer.createContext("/subtasks", new SubTasksHandler(manager, gson, jsonBuilder)); + httpServer.createContext("/history", new HistoryHandler(manager, gson, jsonBuilder)); + httpServer.createContext("/prioritized", new PrioritizedHandler(manager, gson, jsonBuilder)); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void start() { + try { + httpServer.start(); + System.out.println("Server is running: " + httpServer.getAddress().getPort()); + } catch (IllegalStateException e) { + System.err.println("Server is already running"); + } + } + + public void stop() { + httpServer.stop(0); + System.out.println("Server has been stopped"); + } +} From 869f3c224e1cf0ecf0b80d556a6709eca1a1d5a9 Mon Sep 17 00:00:00 2001 From: Crodi Date: Tue, 23 Sep 2025 21:29:52 +0300 Subject: [PATCH 11/22] feat: Handlers for http server --- src/http/handlers/BaseHttpHandler.java | 159 ++++++++++++++++++++++ src/http/handlers/EpicsHandler.java | 135 ++++++++++++++++++ src/http/handlers/HistoryHandler.java | 51 +++++++ src/http/handlers/PrioritizedHandler.java | 33 +++++ src/http/handlers/SubTasksHandler.java | 103 ++++++++++++++ src/http/handlers/TaskHandler.java | 107 +++++++++++++++ 6 files changed, 588 insertions(+) create mode 100644 src/http/handlers/BaseHttpHandler.java create mode 100644 src/http/handlers/EpicsHandler.java create mode 100644 src/http/handlers/HistoryHandler.java create mode 100644 src/http/handlers/PrioritizedHandler.java create mode 100644 src/http/handlers/SubTasksHandler.java create mode 100644 src/http/handlers/TaskHandler.java diff --git a/src/http/handlers/BaseHttpHandler.java b/src/http/handlers/BaseHttpHandler.java new file mode 100644 index 0000000..faf8997 --- /dev/null +++ b/src/http/handlers/BaseHttpHandler.java @@ -0,0 +1,159 @@ +package http.handlers; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import managers.TaskManager; +import util.http.JsonBuilder; +import util.http.RequestSegments; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import static util.enums.Endpoint.*; +import static util.http.RequestSegments.getRequestSegments; + +public abstract class BaseHttpHandler implements HttpHandler { + private final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + protected final TaskManager manager; + + protected Gson gson; + protected JsonBuilder jsonBuilder; + + + public BaseHttpHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) { + this.manager = manager; + this.gson = gson; + + this.jsonBuilder = jsonBuilder; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + RequestSegments segments = getPreparedSegments(exchange); + + if (segments == null || validateResources(exchange, segments)) { + return; + } + + mapEndpoints(exchange, segments); + + } catch (JsonSyntaxException e) { + sendText(exchange, jsonBuilder.badRequest( + "Json must contain: taskId, title, description and status[NEW, IN_PROGRESS, DONE]. " + + "If subtasks is resource: epicId. " + + "If being added taskId must be 0."), 400); + } catch (IOException e) { + e.printStackTrace(); + } catch (Exception e) { + try { + sendServerError(exchange); + e.printStackTrace(); + } catch (IOException ex) { + System.out.println("Failed to send response"); + } + } + } + + private RequestSegments getPreparedSegments(HttpExchange exchange) throws IOException { + RequestSegments segments = getRequestSegments(exchange); + + jsonBuilder.setSegments(segments); + + if (!isSegmentsValid(exchange, segments)) { + return null; + } + + return segments; + } + + private void mapEndpoints(HttpExchange exchange, RequestSegments segments) throws IOException { + switch (segments.endpoint()) { + case OPTIONS -> handleOptions(exchange, segments); + case GET -> handleGet(exchange, segments); + case POST -> handlePost(exchange, segments); + case DELETE -> handleDelete(exchange, segments); + default -> sendText(exchange, jsonBuilder.badRequest("No such endpoint"), 400); + } + } + + private boolean isSegmentsValid(HttpExchange exchange, RequestSegments segments) throws IOException { + System.out.printf("Validating segments: %s Method: %s\n", segments, exchange.getRequestMethod()); + + if (segments.endpoint() == INVALID_METHOD) { + exchange.getResponseHeaders().add("Allow", "GET, POST, DELETE, HEAD, OPTIONS"); + sendMethodNotAllowed(exchange); + return false; + } else if (segments.endpoint() == INVALID) { + sendText(exchange, jsonBuilder.badRequest("No such endpoint"), 400); + return false; + } else if (segments.endpoint() == INVALID_SUBRESOURCE) { + sendNotFound(exchange, jsonBuilder.tooMuchSubResources()); + return false; + } else if (segments.id() == -1) { + sendText(exchange, jsonBuilder.invalidId(), 400); + return false; + } else if (segments.subResource().isPresent() && !segments.resource().equals("epics")) { + sendNotFound(exchange, jsonBuilder.subresourceNotFound()); + return false; + } + + return true; + } + + + protected void sendText(HttpExchange exchange, String responseString, int responseCode) throws IOException { + byte[] resp = responseString.getBytes(DEFAULT_CHARSET); + exchange.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); + exchange.getResponseHeaders().add("Content-Length", String.valueOf(resp.length)); + + if (!"HEAD".equals(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(responseCode, resp.length); + exchange.getResponseBody().write(resp); + } else { + exchange.sendResponseHeaders(responseCode, -1); + } + + exchange.close(); + } + + protected void sendNotFound(HttpExchange exchange, String responseString) throws IOException { + sendText(exchange, responseString, 404); + } + + protected void sendHasOverlaps(HttpExchange exchange, String responseString) throws IOException { + sendText(exchange, responseString, 406); + } + + protected void sendMethodNotAllowed(HttpExchange exchange) throws IOException { + exchange.sendResponseHeaders(405, -1); + exchange.close(); + } + + protected void sendServerError(HttpExchange exchange) throws IOException { + exchange.sendResponseHeaders(500, -1); + exchange.close(); + } + + protected boolean isPostForbidden(HttpExchange exchange, RequestSegments segments) throws IOException { + if (segments.id() != 0) { + exchange.getResponseHeaders().add("Allow", "GET, DELETE, OPTIONS, HEAD"); + sendMethodNotAllowed(exchange); + return true; + } + return false; + } + + protected abstract void handleOptions(HttpExchange exchange, RequestSegments segments) throws IOException; + + protected abstract void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException; + + protected abstract void handlePost(HttpExchange exchange, RequestSegments segments) throws IOException; + + protected abstract void handleDelete(HttpExchange exchange, RequestSegments segments) throws IOException; + + protected abstract boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException; +} diff --git a/src/http/handlers/EpicsHandler.java b/src/http/handlers/EpicsHandler.java new file mode 100644 index 0000000..9c06d6a --- /dev/null +++ b/src/http/handlers/EpicsHandler.java @@ -0,0 +1,135 @@ +package http.handlers; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.sun.net.httpserver.HttpExchange; +import managers.TaskManager; +import model.Epic; +import model.Task; +import util.enums.Endpoint; +import util.exceptions.TaskNotFound; +import util.http.JsonBuilder; +import util.http.RequestSegments; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class EpicsHandler extends TaskHandler { + + public EpicsHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) { + super(manager, gson, jsonBuilder); + } + + @Override + protected void handleOptions(HttpExchange exchange, RequestSegments segments) throws IOException { + if (segments.id() == 0 && segments.subResource().isEmpty()) { + exchange.getResponseHeaders().add("Allow", "GET, POST, DELETE, OPTIONS, HEAD"); + } else { + exchange.getResponseHeaders().add("Allow", "GET, DELETE, OPTIONS, HEAD"); + } + exchange.sendResponseHeaders(204, -1); + } + + @Override + protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException { + int id = segments.id(); + + if (segments.subResource().isPresent()) { + if (manager.getEpicWithoutHistory(id) != null) { + sendText(exchange, gson.toJson(manager.getSubTasksFromEpic(id)), 200); + } else { + sendNotFound(exchange, jsonBuilder.notFound("Epic with id '" + id + "'not found")); + } + } else if (id == 0) { + sendText(exchange, gson.toJson(manager.getEpics()), 200); + } else { + Epic epic = manager.getEpic(id); + if (epic != null) { + sendText(exchange, gson.toJson(epic), 200); + } else { + sendNotFound(exchange, jsonBuilder.notFound("Epic with id '" + id + "'not found")); + } + } + } + + @Override + protected void handlePost(HttpExchange exchange, RequestSegments segments) throws IOException { + if (isPostForbidden(exchange, segments)) { + return; + } + String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + + if (body.isBlank()) { + sendText(exchange, jsonBuilder.badRequest("Body is required"), 400); + } + try { + Epic epic = gson.fromJson(body, Epic.class); + isTaskValid(epic); + + if (epic.getTaskId() == 0) { + //новый эпик для безопасного создания + epic = new Epic(epic.getTitle(), epic.getDescription(), epic.getStatus()); + manager.addEpic(epic); + } else { + manager.updateEpic(epic); + } + sendText(exchange, gson.toJson(manager.getEpicWithoutHistory(epic.getTaskId())), 201); + } catch (JsonSyntaxException e) { + sendText(exchange, + jsonBuilder.badRequest( + "Json must contain: taskId, title, description and status[NEW, IN_PROGRESS, DONE]."), + 400); + } catch (TaskNotFound e) { + sendNotFound(exchange, jsonBuilder.notFound(e.getMessage())); + } + } + + @Override + protected void handleDelete(HttpExchange exchange, RequestSegments segments) throws IOException { + + if (segments.subResource().isPresent()) { + try { + manager.clearSubTasksFromEpic(segments.id()); + sendText(exchange, + jsonBuilder.message("Subtasks from epic: '" + segments.id() + "' were deleted"), + 200); + } catch (TaskNotFound e) { + sendNotFound(exchange, jsonBuilder.notFound(e.getMessage())); + } + } else if (segments.id() == 0) { + manager.clearEpics(); + sendText(exchange, jsonBuilder.message("Epics were deleted"), 200); + } else { + manager.deleteEpic(segments.id()); + sendText(exchange, jsonBuilder.message("Epic: '" + segments.id() + "' was deleted"), 200); + } + } + + @Override + protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException { + if (!"epics".equals(segments.resource())) { + sendNotFound(exchange, jsonBuilder.resourceNotFound()); + return true; + } + + if (segments.subResource().isPresent()) { + if (!segments.subResource().get().equals("subtasks")) { + sendNotFound(exchange, jsonBuilder.subresourceNotFound()); + return true; + } + if (segments.endpoint() == Endpoint.POST) { + exchange.getResponseHeaders().add("Allow", "GET, DELETE, HEAD, OPTIONS"); + sendMethodNotAllowed(exchange); + return true; + } + } + return false; + } + + @Override + protected void isTaskValid(Task task) { + if (task.getTitle() == null || task.getDescription() == null) { + throw new JsonSyntaxException("Epic has invalid fields"); + } + } +} diff --git a/src/http/handlers/HistoryHandler.java b/src/http/handlers/HistoryHandler.java new file mode 100644 index 0000000..ff62596 --- /dev/null +++ b/src/http/handlers/HistoryHandler.java @@ -0,0 +1,51 @@ +package http.handlers; + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import managers.TaskManager; +import util.http.JsonBuilder; +import util.http.RequestSegments; + +import java.io.IOException; + +public class HistoryHandler extends BaseHttpHandler { + + public HistoryHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) { + super(manager, gson, jsonBuilder); + } + + @Override + protected void handleOptions(HttpExchange exchange, RequestSegments segments) throws IOException { + exchange.getResponseHeaders().add("Allow", "GET, OPTIONS, HEAD"); + exchange.sendResponseHeaders(204, -1); + } + + @Override + protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException { + sendText(exchange, gson.toJson(manager.getHistory()), 200); + } + + @Override + protected void handlePost(HttpExchange exchange, RequestSegments segments) throws IOException { + exchange.getResponseHeaders().add("Allow", "GET, HEAD, OPTIONS"); + sendMethodNotAllowed(exchange); + } + + @Override + protected void handleDelete(HttpExchange exchange, RequestSegments segments) throws IOException { + exchange.getResponseHeaders().add("Allow", "GET, HEAD, OPTIONS"); + sendMethodNotAllowed(exchange); + } + + @Override + protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException { + if (!"history".equals(segments.resource())) { + sendNotFound(exchange, jsonBuilder.resourceNotFound()); + return true; + } else if (segments.id() != 0) { + sendNotFound(exchange, jsonBuilder.subresourceNotFound()); + return true; + } + return false; + } +} diff --git a/src/http/handlers/PrioritizedHandler.java b/src/http/handlers/PrioritizedHandler.java new file mode 100644 index 0000000..dd2a991 --- /dev/null +++ b/src/http/handlers/PrioritizedHandler.java @@ -0,0 +1,33 @@ +package http.handlers; + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import managers.TaskManager; +import util.http.JsonBuilder; +import util.http.RequestSegments; + +import java.io.IOException; + +public class PrioritizedHandler extends HistoryHandler { + + public PrioritizedHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) { + super(manager, gson, jsonBuilder); + } + + @Override + protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException { + sendText(exchange, gson.toJson(manager.getPrioritizedTasks()), 200); + } + + @Override + protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException { + if (!"prioritized".equals(segments.resource())) { + sendNotFound(exchange, jsonBuilder.resourceNotFound()); + return true; + } else if (segments.id() != 0) { + sendNotFound(exchange, jsonBuilder.subresourceNotFound()); + return true; + } + return false; + } +} diff --git a/src/http/handlers/SubTasksHandler.java b/src/http/handlers/SubTasksHandler.java new file mode 100644 index 0000000..392ebd1 --- /dev/null +++ b/src/http/handlers/SubTasksHandler.java @@ -0,0 +1,103 @@ +package http.handlers; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.sun.net.httpserver.HttpExchange; +import managers.TaskManager; +import model.SubTask; +import model.Task; +import util.exceptions.TaskNotFound; +import util.exceptions.TaskTimeOverlapException; +import util.http.JsonBuilder; +import util.http.RequestSegments; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class SubTasksHandler extends TaskHandler { + + public SubTasksHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) { + super(manager, gson, jsonBuilder); + } + + @Override + protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException { + if (segments.id() == 0) { + sendText(exchange, gson.toJson(manager.getSubTasks()), 200); + } else { + SubTask subtask = manager.getSubTask(segments.id()); + if (subtask != null) { + sendText(exchange, gson.toJson(subtask), 200); + } else { + sendNotFound(exchange, jsonBuilder.notFound("SubTask with id '" + segments.id() + "'not found")); + } + } + } + + @Override + protected void handlePost(HttpExchange exchange, RequestSegments segments) throws IOException { + if (isPostForbidden(exchange, segments)) { + return; + } + String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + + if (body.isBlank()) { + sendText(exchange, jsonBuilder.badRequest("Body is required"), 400); + } + + try { + SubTask subTask = gson.fromJson(body, SubTask.class); + isTaskValid(subTask); + + if (subTask.getTaskId() == 0) { + manager.addSubTask(subTask); + } else { + manager.updateSubTask(subTask); + } + sendText(exchange, gson.toJson(manager.getSubTaskWithoutHistory(subTask.getTaskId())), 201); + } catch (JsonSyntaxException e) { + sendText(exchange, + jsonBuilder.badRequest( + "Json must contain: taskId, title, description, status," + + " status[NEW, IN_PROGRESS, DONE], epicId."), + 400); + } catch (TaskNotFound e) { + sendNotFound(exchange, jsonBuilder.notFound(e.getMessage())); + } catch (TaskTimeOverlapException e) { + sendHasOverlaps(exchange, jsonBuilder.hasOverlaps()); + } + + } + + @Override + protected void handleDelete(HttpExchange exchange, RequestSegments segments) throws IOException { + if (segments.id() == 0) { + manager.clearSubTasks(); + sendText(exchange, jsonBuilder.message("Subtasks were deleted"), 200); + } else { + manager.deleteSubTask(segments.id()); + sendText(exchange, jsonBuilder.message("Subtask: '" + segments.id() + "' was deleted"), 200); + } + } + + @Override + protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException { + if (!"subtasks".equals(segments.resource())) { + sendNotFound(exchange, jsonBuilder.resourceNotFound()); + return true; + } + return false; + } + + @Override + protected void isTaskValid(Task task) { + SubTask subTask = (SubTask) task; + if (subTask.getTitle() == null || + subTask.getDescription() == null || + subTask.getStatus() == null || + subTask.getEpicId() == 0) { + throw new JsonSyntaxException("Subtask has invalid fields"); + } + } + +} diff --git a/src/http/handlers/TaskHandler.java b/src/http/handlers/TaskHandler.java new file mode 100644 index 0000000..ffcb6fd --- /dev/null +++ b/src/http/handlers/TaskHandler.java @@ -0,0 +1,107 @@ +package http.handlers; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.sun.net.httpserver.HttpExchange; +import managers.TaskManager; +import model.Task; +import util.exceptions.TaskNotFound; +import util.exceptions.TaskTimeOverlapException; +import util.http.JsonBuilder; +import util.http.RequestSegments; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class TaskHandler extends BaseHttpHandler { + + public TaskHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) { + super(manager, gson, jsonBuilder); + } + + @Override + protected void handleOptions(HttpExchange exchange, RequestSegments segments) throws IOException { + if (segments.id() == 0) { + exchange.getResponseHeaders().add("Allow", "GET, POST, DELETE, OPTIONS, HEAD"); + } else { + exchange.getResponseHeaders().add("Allow", "GET, DELETE, OPTIONS, HEAD"); + } + exchange.sendResponseHeaders(204, -1); + } + + + @Override + protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException { + + if (segments.id() == 0) { + sendText(exchange, gson.toJson(manager.getTasks()), 200); + } else { + Task task = manager.getTask(segments.id()); + if (task != null) { + sendText(exchange, gson.toJson(task), 200); + } else { + sendNotFound(exchange, jsonBuilder.notFound("Task with id '" + segments.id() + "'not found")); + } + } + } + + @Override + protected void handlePost(HttpExchange exchange, RequestSegments segments) throws IOException { + if (isPostForbidden(exchange, segments)) { + return; + } + String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + + if (body.isBlank()) { + sendText(exchange, jsonBuilder.badRequest("Body is required"), 400); + } + + try { + Task task = gson.fromJson(body, Task.class); + isTaskValid(task); + + if (task.getTaskId() == 0) { + manager.addTask(task); + } else { + manager.updateTask(task); + } + sendText(exchange, gson.toJson(manager.getTaskWithoutHistory(task.getTaskId())), 201); + } catch (JsonSyntaxException e) { + System.out.println(e); + sendText(exchange, + jsonBuilder.badRequest( + "Json must contain: taskId, title, description and status[NEW, IN_PROGRESS, DONE]."), + 400); + } catch (TaskNotFound e) { + sendNotFound(exchange, jsonBuilder.notFound(e.getMessage())); + } catch (TaskTimeOverlapException e) { + sendHasOverlaps(exchange, jsonBuilder.hasOverlaps()); + } + } + + @Override + protected void handleDelete(HttpExchange exchange, RequestSegments segments) throws IOException { + if (segments.id() == 0) { + manager.clearTasks(); + sendText(exchange, jsonBuilder.message("Tasks were deleted"), 200); + } else { + manager.deleteTask(segments.id()); + sendText(exchange, jsonBuilder.message("Task: '" + segments.id() + "' was deleted"), 200); + } + } + + @Override + protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException { + if (!"tasks".equals(segments.resource())) { + sendNotFound(exchange, jsonBuilder.resourceNotFound()); + return true; + } + return false; + } + + protected void isTaskValid(Task task) { + if (task.getTitle() == null || task.getDescription() == null || task.getStatus() == null) { + throw new JsonSyntaxException("Task has invalid fields"); + } + } +} From 693ea9c6c2212d7d36fd6f7cb253385385fd5527 Mon Sep 17 00:00:00 2001 From: Crodi Date: Tue, 23 Sep 2025 21:30:07 +0300 Subject: [PATCH 12/22] test: add test --- test/http/HttpTaskServerTest.java | 151 +++++++ test/http/handlers/EpicsHandlerTest.java | 393 ++++++++++++++++++ test/http/handlers/HistoryHandlerTest.java | 203 +++++++++ .../http/handlers/PrioritizedHandlerTest.java | 73 ++++ test/http/handlers/SubTasksHandlerTest.java | 245 +++++++++++ test/http/handlers/TaskHandlerTest.java | 332 +++++++++++++++ 6 files changed, 1397 insertions(+) create mode 100644 test/http/HttpTaskServerTest.java create mode 100644 test/http/handlers/EpicsHandlerTest.java create mode 100644 test/http/handlers/HistoryHandlerTest.java create mode 100644 test/http/handlers/PrioritizedHandlerTest.java create mode 100644 test/http/handlers/SubTasksHandlerTest.java create mode 100644 test/http/handlers/TaskHandlerTest.java diff --git a/test/http/HttpTaskServerTest.java b/test/http/HttpTaskServerTest.java new file mode 100644 index 0000000..31b14d7 --- /dev/null +++ b/test/http/HttpTaskServerTest.java @@ -0,0 +1,151 @@ +package http; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import managers.InMemoryTaskManager; +import managers.TaskManager; +import model.Epic; +import model.SubTask; +import model.Task; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import util.enums.Status; +import util.gsonadapters.DurationAdapter; +import util.gsonadapters.LocalDateTimeAdapter; + +import java.io.IOException; +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; + +public class HttpTaskServerTest { + + protected HttpClient client; + protected HttpTaskServer server; + protected TaskManager manager; + protected final Gson gson = new GsonBuilder() + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter()) + .registerTypeAdapter(Duration.class, new DurationAdapter()) + .create(); + + protected final LocalDateTime epochTime = + LocalDateTime.of(1970, 1, 1, 0, 0, 0); + + protected Task task; + protected Epic epic; + protected SubTask subTask; + + @BeforeEach + public void initServer() { + client = HttpClient.newHttpClient(); + manager = new InMemoryTaskManager(); + server = new HttpTaskServer(manager); + + task = new Task("task1", "demo", Status.NEW); + epic = new Epic("epic1", "demo", Status.NEW); + subTask = new SubTask("subtask1", "demo", Status.NEW, 1); + server.start(); + } + + @AfterEach + public void stopServer() { + server.stop(); + } + + protected HttpRequest getMethod(String method, String path) { + URI url = URI.create(path); + + HttpRequest.Builder builder = HttpRequest.newBuilder().uri(url); + + switch (method) { + case "HEAD" -> builder.HEAD(); + case "OPTIONS" -> builder.method("OPTIONS", HttpRequest.BodyPublishers.noBody()); + case "GET" -> builder.GET(); + case "DELETE" -> builder.DELETE(); + } + + return builder.build(); + } + + protected HttpRequest getPost(String value, String taskJson) { + URI url = URI.create(value); + + return HttpRequest + .newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(taskJson)) + .uri(url) + .build(); + } + + @Nested + class BaseHandlerTest { + private HttpRequest getInvalidMethod() { + URI url = URI.create("http://localhost:8080/tasks"); + + return HttpRequest + .newBuilder() + .PUT(HttpRequest.BodyPublishers.ofString("")) + .uri(url) + .build(); + } + + @Test + public void shouldReturn405WhenForbiddenMethod() throws IOException, InterruptedException { + + HttpResponse response = client.send( + getInvalidMethod(), + HttpResponse.BodyHandlers.ofString() + ); + + assertEquals(405, response.statusCode()); + } + + @Test + public void shouldReturn404WhenTooManySubs() throws IOException, InterruptedException { + + HttpResponse response = client.send( + getMethod("GET", "http://localhost:8080/tasks/1/sub/sub"), + HttpResponse.BodyHandlers.ofString() + ); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn405WhenInvalidMethod() throws IOException, InterruptedException { + HttpRequest request = HttpRequest + .newBuilder() + .method("BlaBlaBla", HttpRequest.BodyPublishers.noBody()) + .uri(URI.create("http://localhost:8080/tasks")) + .build(); + + + HttpResponse response = client.send( + request, + HttpResponse.BodyHandlers.ofString() + ); + + assertEquals(405, response.statusCode()); + } + } + + public class ListTaskTypeToken extends TypeToken> { + } + + public class ListEpicTypeToken extends TypeToken> { + } + + public class ListSubTaskTypeToken extends TypeToken> { + } +} + + diff --git a/test/http/handlers/EpicsHandlerTest.java b/test/http/handlers/EpicsHandlerTest.java new file mode 100644 index 0000000..1aac956 --- /dev/null +++ b/test/http/handlers/EpicsHandlerTest.java @@ -0,0 +1,393 @@ +package http.handlers; + +import http.HttpTaskServerTest; +import model.Epic; +import model.SubTask; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import util.enums.Status; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class EpicsHandlerTest extends HttpTaskServerTest { + + @Nested + class EpicHeadTest { + private HttpRequest getRequest(String value) { + URI url = URI.create(value); + + return HttpRequest + .newBuilder() + .HEAD() + .uri(url) + .build(); + } + + @Test + public void shouldReturn200AndRightHeadersWhenHeadWithEmptyEpics() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics"), + HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.headers().firstValue("Content-type")); + assertEquals(200, response.statusCode()); + + assertEquals("application/json;charset=utf-8", + response.headers().firstValue("Content-type").get()); + assertEquals("2", + response.headers().firstValue("Content-length").get()); + } + + @Test + public void shouldReturn404AndRightHeadersWhenHeadWithInvalidEpic() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics/1"), + HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.headers().firstValue("Content-type")); + assertEquals(404, response.statusCode()); + + assertEquals("application/json;charset=utf-8", + response.headers().firstValue("Content-type").get()); + assertEquals("69", + response.headers().firstValue("Content-length").get()); + } + + @Test + public void shouldReturn200AndRightHeadersWhenHeadWithSubResource() throws IOException, InterruptedException { + manager.addEpic(epic); + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics/1/subtasks"), + HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.headers().firstValue("Content-type")); + assertEquals(200, response.statusCode()); + + assertEquals("application/json;charset=utf-8", + response.headers().firstValue("Content-type").get()); + assertEquals("2", + response.headers().firstValue("Content-length").get()); + } + + @Test + public void shouldReturn400AndRightHeadersWhenHeadWithInvalidId() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics/-1"), + HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.headers().firstValue("Content-type")); + assertEquals(400, response.statusCode()); + + assertEquals("application/json;charset=utf-8", + response.headers().firstValue("Content-type").get()); + assertEquals("65", + response.headers().firstValue("Content-length").get()); + } + + } + + @Nested + class EpicGetTest { + + private HttpRequest getRequest(String value) { + return getMethod("GET", value); + } + + @Test + public void shouldReturn404WhenInvalidResource() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/epicsABC"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldBeEqualAndCode200() throws IOException, InterruptedException { + manager.addEpic(epic); + + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics/1"), + HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.body()); + assertEquals(200, response.statusCode()); + assertEquals(epic, gson.fromJson(response.body(), Epic.class)); + } + + @Test + public void shouldSameAmountAndCode200() throws IOException, InterruptedException { + manager.addEpic(epic); + manager.addEpic(new Epic("epic2", "demo", Status.NEW)); + + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics"), + HttpResponse.BodyHandlers.ofString()); + + List list = gson.fromJson(response.body(), new ListEpicTypeToken().getType()); + + assertEquals(200, response.statusCode()); + assertEquals(2, list.size()); + } + + @Test + public void shouldReturn404WhenNoId() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics/1"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn404WhenSubResourcePresent() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics/1/sub"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn404WhenIdInvalid() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics/-2a"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(400, response.statusCode()); + } + + @Test + public void shouldReturn200AndSubtasks() throws IOException, InterruptedException { + manager.addEpic(epic); + manager.addSubTask(subTask); + + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics/1/subtasks"), + HttpResponse.BodyHandlers.ofString()); + + List list = gson.fromJson(response.body(), new ListSubTaskTypeToken().getType()); + + assertEquals(200, response.statusCode()); + assertEquals(1, list.size()); + assertEquals(subTask, list.getFirst()); + + } + + @Test + public void shouldReturn404WhenNoIdAndSubTasks() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics/1/subtasks"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + } + + @Nested + class EpicPostTest { + + @Test + public void shouldBeEqualAndCode201() throws IOException, InterruptedException { + String taskJson = gson.toJson(epic); + + HttpResponse response = client.send( + getPost("http://localhost:8080/epics", taskJson), + HttpResponse.BodyHandlers.ofString()); + + epic.setTaskId(1); + + assertEquals(201, response.statusCode()); + assertEquals(epic, manager.getEpic(1)); + } + + @Test + public void shouldBeUpdatedAndCode201() throws IOException, InterruptedException { + manager.addEpic(epic); + + epic.setTitle("new title"); + String taskJson = gson.toJson(epic); + + HttpResponse response = client.send( + getPost("http://localhost:8080/epics", taskJson), + HttpResponse.BodyHandlers.ofString()); + + + assertEquals(201, response.statusCode()); + assertEquals(epic, manager.getEpic(1)); + } + + @Test + public void shouldNotBeUpdatedAndCode404WhenNotFound() throws IOException, InterruptedException { + + epic.setTaskId(1); + String taskJson = gson.toJson(epic); + + HttpResponse response = client.send( + getPost("http://localhost:8080/epics", taskJson), + HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.body()); + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn405AndNotPostWhenIdAndSubIsPresent() throws IOException, InterruptedException { + String taskJson = gson.toJson(epic); + + HttpResponse response = client.send( + getPost("http://localhost:8080/epics/1/subtasks", taskJson), + HttpResponse.BodyHandlers.ofString() + ); + epic.setTaskId(1); + + assertEquals(405, response.statusCode()); + assertNull(manager.getEpic(1)); + } + + @Test + public void shouldReturn400WhenNoBody() throws IOException, InterruptedException { + + HttpResponse response = client.send( + getPost("http://localhost:8080/epics", ""), + HttpResponse.BodyHandlers.ofString() + ); + epic.setTaskId(1); + + assertEquals(400, response.statusCode()); + assertNull(manager.getEpic(1)); + } + + @Test + public void shouldReturn400WhenInvalidBody() throws IOException, InterruptedException { + String taskJson = "{blablabla:blabla}"; + + HttpResponse response = client.send( + getPost("http://localhost:8080/epics", taskJson), + HttpResponse.BodyHandlers.ofString() + ); + epic.setTaskId(1); + + assertEquals(400, response.statusCode()); + assertNull(manager.getEpic(1)); + } + + } + + @Nested + class EpicDeleteTest { + private HttpRequest getRequest(String value) { + return getMethod("DELETE", value); + } + + @Test + public void shouldReturn404WhenIdIsNotPresentAndDeletingSubtasks() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics/1/subtasks"), + HttpResponse.BodyHandlers.ofString() + ); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn200AndDeleteAllEpics() throws IOException, InterruptedException { + manager.addEpic(epic); + manager.addEpic(new Epic("epic2", "demo", Status.NEW)); + + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics"), + HttpResponse.BodyHandlers.ofString() + ); + + assertEquals(200, response.statusCode()); + assertEquals(0, manager.getEpics().size()); + } + + @Test + public void shouldReturn200AndDeleteEpic() throws IOException, InterruptedException { + manager.addEpic(epic); + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics/1"), + HttpResponse.BodyHandlers.ofString() + ); + + assertEquals(200, response.statusCode()); + assertNull(manager.getEpic(1)); + } + + @Test + public void shouldReturn200AndDeleteSubtasksFromEpic() throws IOException, InterruptedException { + manager.addEpic(epic); + manager.addSubTask(subTask); + + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics/1/subtasks"), + HttpResponse.BodyHandlers.ofString() + ); + + assertEquals(200, response.statusCode()); + assertEquals(0, manager.getSubTasksFromEpic(1).size()); + } + + + } + + @Nested + class EpicOptionsTest { + + private HttpRequest getRequest(String value) { + return getMethod("OPTIONS", value); + } + + @Test + public void shouldReturn204AndValidMethods() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics"), + HttpResponse.BodyHandlers.ofString() + ); + List expected = List.of("GET, POST, DELETE, OPTIONS, HEAD"); + + List methods = response.headers().allValues("Allow"); + + assertEquals(204, response.statusCode()); + assertEquals(expected, methods); + } + + @Test + public void shouldReturn204AndValidMethodsWhenIdIsPresent() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasks/1"), + HttpResponse.BodyHandlers.ofString() + ); + List expected = List.of("GET, DELETE, OPTIONS, HEAD"); + + List methods = response.headers().allValues("Allow"); + + assertEquals(204, response.statusCode()); + assertEquals(expected, methods); + } + + + @Test + public void shouldReturn204AndValidMethodsWhenIdAndSubTasksIsPresent() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/epics/1/subtasks"), + HttpResponse.BodyHandlers.ofString() + ); + List expected = List.of("GET, DELETE, OPTIONS, HEAD"); + + List methods = response.headers().allValues("Allow"); + + assertEquals(204, response.statusCode()); + assertEquals(expected, methods); + } + } +} \ No newline at end of file diff --git a/test/http/handlers/HistoryHandlerTest.java b/test/http/handlers/HistoryHandlerTest.java new file mode 100644 index 0000000..c7530df --- /dev/null +++ b/test/http/handlers/HistoryHandlerTest.java @@ -0,0 +1,203 @@ +package http.handlers; + +import http.HttpTaskServerTest; +import model.Task; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class HistoryHandlerTest extends HttpTaskServerTest { + + @BeforeEach + public void addTasks() { + manager.addEpic(epic); + manager.addTask(task); + manager.addSubTask(subTask); + } + + @Nested + class HistoryHeadTest { + + private HttpRequest getRequest(String value) { + return getMethod("HEAD", value); + } + + @Test + public void shouldReturn200AndRightHeadersWhenHeadWithEmptyTasks() throws IOException, InterruptedException { + + HttpResponse response = client.send( + getRequest("http://localhost:8080/history"), + HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.headers().firstValue("Content-type")); + assertEquals(200, response.statusCode()); + + assertEquals("application/json;charset=utf-8", + response.headers().firstValue("Content-type").get()); + assertEquals("2", + response.headers().firstValue("Content-length").get()); + } + + @Test + public void shouldReturn404AndRightHeadersWhenHeadWithInvalidId() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/history/1"), + HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.headers().firstValue("Content-type")); + assertEquals(404, response.statusCode()); + + assertEquals("application/json;charset=utf-8", + response.headers().firstValue("Content-type").get()); + assertEquals("73", + response.headers().firstValue("Content-length").get()); + } + + @Test + public void shouldReturn404AndRightHeadersWhenHeadWithInvalidSubResource() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/history/1/sub"), + HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.headers().firstValue("Content-type")); + assertEquals(404, response.statusCode()); + + assertEquals("application/json;charset=utf-8", + response.headers().firstValue("Content-type").get()); + assertEquals("76", + response.headers().firstValue("Content-length").get()); + } + + @Test + public void shouldReturn400AndRightHeadersWhenHeadWithInvalidId() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/history/-1"), + HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.headers().firstValue("Content-type")); + assertEquals(400, response.statusCode()); + + assertEquals("application/json;charset=utf-8", + response.headers().firstValue("Content-type").get()); + assertEquals("65", + response.headers().firstValue("Content-length").get()); + } + + } + + @Nested + class HistoryGetTest { + + private HttpRequest getRequest(String value) { + return getMethod("GET", value); + } + + @Test + public void shouldBeEqualAndCode200() throws IOException, InterruptedException { + + manager.getEpic(1); + manager.getTask(2); + manager.getSubTask(3); + + HttpResponse response = client.send( + getRequest("http://localhost:8080/history"), + HttpResponse.BodyHandlers.ofString()); + + + List list = gson.fromJson(response.body(), new ListTaskTypeToken().getType()); + assertEquals(200, response.statusCode()); + assertEquals(3, list.size()); + } + + @Test + public void shouldReturn404WithInvalidId() throws IOException, InterruptedException { + + HttpResponse response = client.send( + getRequest("http://localhost:8080/history/1"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn404WithInvalidIdAndSub() throws IOException, InterruptedException { + + HttpResponse response = client.send( + getRequest("http://localhost:8080/history/1/sub"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn404WithInvalidResource() throws IOException, InterruptedException { + + HttpResponse response = client.send( + getRequest("http://localhost:8080/historyABC"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + } + + @Nested + class HistoryPostTest { + + @Test + public void shouldReturn405() throws IOException, InterruptedException { + HttpResponse response = client.send( + getPost("http://localhost:8080/history", ""), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(405, response.statusCode()); + } + } + + @Nested + class HistoryDeleteTest { + + private HttpRequest getRequest(String value) { + return getMethod("DELETE", value); + } + + @Test + public void shouldReturn405() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/history"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(405, response.statusCode()); + } + } + + @Nested + class SubTaskOptionsTest { + + private HttpRequest getRequest(String value) { + return getMethod("OPTIONS", value); + } + + @Test + public void shouldReturn204AndValidMethods() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/history"), + HttpResponse.BodyHandlers.ofString() + ); + List expected = List.of("GET, OPTIONS, HEAD"); + + List methods = response.headers().allValues("Allow"); + + assertEquals(204, response.statusCode()); + assertEquals(expected, methods); + } + } + +} \ No newline at end of file diff --git a/test/http/handlers/PrioritizedHandlerTest.java b/test/http/handlers/PrioritizedHandlerTest.java new file mode 100644 index 0000000..cb9edd4 --- /dev/null +++ b/test/http/handlers/PrioritizedHandlerTest.java @@ -0,0 +1,73 @@ +package http.handlers; + +import http.HttpTaskServerTest; +import model.Task; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import util.enums.Status; + +import java.io.IOException; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PrioritizedHandlerTest extends HttpTaskServerTest { + + @Nested + class HistoryGetTest { + + private HttpRequest getRequest(String value) { + return getMethod("GET", value); + } + + @Test + public void shouldBeEqualAndCode200() throws IOException, InterruptedException { + manager.addTask(new Task("task1", "demo", Status.NEW, 10, epochTime)); + manager.addTask(new Task("task2", "demo", Status.NEW, 10, epochTime.plusMinutes(10))); + manager.addTask(task); + + HttpResponse response = client.send( + getRequest("http://localhost:8080/prioritized"), + HttpResponse.BodyHandlers.ofString()); + + + List list = gson.fromJson(response.body(), new ListTaskTypeToken().getType()); + assertEquals(200, response.statusCode()); + assertEquals(2, list.size()); + } + + @Test + public void shouldReturn404WithInvalidId() throws IOException, InterruptedException { + + HttpResponse response = client.send( + getRequest("http://localhost:8080/prioritized/1"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn404WithInvalidIdAndSub() throws IOException, InterruptedException { + + HttpResponse response = client.send( + getRequest("http://localhost:8080/prioritized/1/sub"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn404WithInvalidResource() throws IOException, InterruptedException { + + HttpResponse response = client.send( + getRequest("http://localhost:8080/prioritizedABC"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + } + +} \ No newline at end of file diff --git a/test/http/handlers/SubTasksHandlerTest.java b/test/http/handlers/SubTasksHandlerTest.java new file mode 100644 index 0000000..801bfa9 --- /dev/null +++ b/test/http/handlers/SubTasksHandlerTest.java @@ -0,0 +1,245 @@ +package http.handlers; + +import http.HttpTaskServerTest; +import model.SubTask; +import model.Task; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import util.enums.Status; + +import java.io.IOException; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class SubTasksHandlerTest extends HttpTaskServerTest { + + @BeforeEach + public void addEpic() { + manager.addEpic(epic); + } + + @Nested + class SubTaskGetTest { + + private HttpRequest getRequest(String value) { + return getMethod("GET", value); + } + + @Test + public void shouldReturn404WhenInvalidResource() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/subtasksABC"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldBeEqualAndCode200() throws IOException, InterruptedException { + manager.addSubTask(subTask); + + HttpResponse response = client.send( + getRequest("http://localhost:8080/subtasks/2"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode()); + assertEquals(subTask, gson.fromJson(response.body(), SubTask.class)); + } + + @Test + public void shouldSameAmountAndCode200() throws IOException, InterruptedException { + manager.addSubTask(subTask); + manager.addSubTask(new SubTask("subtask2", "demo", Status.NEW, 1)); + + HttpResponse response = client.send( + getRequest("http://localhost:8080/subtasks"), + HttpResponse.BodyHandlers.ofString()); + + List list = gson.fromJson(response.body(), new ListSubTaskTypeToken().getType()); + + assertEquals(200, response.statusCode()); + assertEquals(2, list.size()); + } + + @Test + public void shouldReturn404WhenNoId() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/subtasks/2"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn404WhenSubResourcePresent() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/subtasks/2/sub"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn404WhenIdInvalid() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/subtasks/-2a"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(400, response.statusCode()); + } + + } + + @Nested + class SubTaskPostTest { + + @Test + public void shouldBeEqualAndCode201() throws IOException, InterruptedException { + String taskJson = gson.toJson(subTask); + + HttpResponse response = client.send( + getPost("http://localhost:8080/subtasks", taskJson), + HttpResponse.BodyHandlers.ofString()); + + subTask.setTaskId(2); + + assertEquals(201, response.statusCode()); + assertEquals(subTask, manager.getSubTask(2)); + } + + @Test + public void shouldBeUpdatedAndCode201() throws IOException, InterruptedException { + manager.addSubTask(subTask); + + subTask.setTitle("new title"); + String taskJson = gson.toJson(subTask); + + HttpResponse response = client.send( + getPost("http://localhost:8080/subtasks", taskJson), + HttpResponse.BodyHandlers.ofString()); + + + assertEquals(201, response.statusCode()); + assertEquals(subTask, manager.getSubTask(2)); + } + + @Test + public void shouldNotBeUpdatedAndCode404WhenNotFound() throws IOException, InterruptedException { + + subTask.setTaskId(2); + String taskJson = gson.toJson(subTask); + + HttpResponse response = client.send( + getPost("http://localhost:8080/subtasks", taskJson), + HttpResponse.BodyHandlers.ofString()); + + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn405AndNotPostWhenIdIsPresent() throws IOException, InterruptedException { + String taskJson = gson.toJson(subTask); + + HttpResponse response = client.send( + getPost("http://localhost:8080/subtasks/2", taskJson), + HttpResponse.BodyHandlers.ofString() + ); + subTask.setTaskId(2); + + assertEquals(405, response.statusCode()); + assertNull(manager.getSubTask(2)); + } + + @Test + public void shouldReturn400WhenNoBody() throws IOException, InterruptedException { + + HttpResponse response = client.send( + getPost("http://localhost:8080/subtasks", ""), + HttpResponse.BodyHandlers.ofString() + ); + subTask.setTaskId(2); + + assertEquals(400, response.statusCode()); + assertNull(manager.getSubTask(2)); + } + + @Test + public void shouldReturn400WhenInvalidBody() throws IOException, InterruptedException { + String taskJson = "{blablabla:blabla}"; + + HttpResponse response = client.send( + getPost("http://localhost:8080/subtasks", taskJson), + HttpResponse.BodyHandlers.ofString() + ); + subTask.setTaskId(2); + + assertEquals(400, response.statusCode()); + assertNull(manager.getSubTask(2)); + } + + @Test + public void shouldReturn406WhenTimeOverlap() throws IOException, InterruptedException { + + manager.addSubTask(new SubTask("subtask1", + "demo", + Status.NEW, + 1, + 10, + epochTime)); + + String taskJson = gson.toJson(new SubTask("subtask2", + "demo", + Status.NEW, + 1, + 5, + epochTime)); + + HttpResponse response = client.send( + getPost("http://localhost:8080/subtasks", taskJson), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(406, response.statusCode()); + } + } + + @Nested + class SubTaskDeleteTest { + + private HttpRequest getRequest(String value) { + return getMethod("DELETE", value); + } + + @Test + public void shouldReturn200AndDeleteAllTasks() throws IOException, InterruptedException { + manager.addSubTask(subTask); + manager.addSubTask(new SubTask("subtask2", "demo", Status.NEW, 1)); + + HttpResponse response = client.send( + getRequest("http://localhost:8080/subtasks"), + HttpResponse.BodyHandlers.ofString() + ); + + assertEquals(200, response.statusCode()); + assertEquals(0, manager.getSubTasks().size()); + } + + @Test + public void shouldReturn200AndDeleteTask() throws IOException, InterruptedException { + manager.addSubTask(subTask); + HttpResponse response = client.send( + getRequest("http://localhost:8080/subtasks/2"), + HttpResponse.BodyHandlers.ofString() + ); + + assertEquals(200, response.statusCode()); + assertNull(manager.getSubTask(2)); + } + } + +} \ No newline at end of file diff --git a/test/http/handlers/TaskHandlerTest.java b/test/http/handlers/TaskHandlerTest.java new file mode 100644 index 0000000..29b1692 --- /dev/null +++ b/test/http/handlers/TaskHandlerTest.java @@ -0,0 +1,332 @@ +package http.handlers; + +import http.HttpTaskServerTest; +import model.Task; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import util.enums.Status; + +import java.io.IOException; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + + +class TaskHandlerTest extends HttpTaskServerTest { + + @Nested + class TaskHeadTest { + + private HttpRequest getRequest(String value) { + return getMethod("HEAD", value); + } + + @Test + public void shouldReturn200AndRightHeadersWhenHeadWithEmptyTasks() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasks"), + HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.headers().firstValue("Content-type")); + assertEquals(200, response.statusCode()); + + assertEquals("application/json;charset=utf-8", + response.headers().firstValue("Content-type").get()); + assertEquals("2", + response.headers().firstValue("Content-length").get()); + } + + @Test + public void shouldReturn404AndRightHeadersWhenHeadWithInvalidTask() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasks/1"), + HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.headers().firstValue("Content-type")); + assertEquals(404, response.statusCode()); + + assertEquals("application/json;charset=utf-8", + response.headers().firstValue("Content-type").get()); + assertEquals("69", + response.headers().firstValue("Content-length").get()); + } + + @Test + public void shouldReturn404AndRightHeadersWhenHeadWithInvalidSubResource() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasks/1/sub"), + HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.headers().firstValue("Content-type")); + assertEquals(404, response.statusCode()); + + assertEquals("application/json;charset=utf-8", + response.headers().firstValue("Content-type").get()); + assertEquals("76", + response.headers().firstValue("Content-length").get()); + } + + @Test + public void shouldReturn400AndRightHeadersWhenHeadWithInvalidId() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasks/-1"), + HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.headers().firstValue("Content-type")); + assertEquals(400, response.statusCode()); + + assertEquals("application/json;charset=utf-8", + response.headers().firstValue("Content-type").get()); + assertEquals("65", + response.headers().firstValue("Content-length").get()); + } + } + + @Nested + class TaskGetTest { + + private HttpRequest getRequest(String value) { + return getMethod("GET", value); + } + + @Test + public void shouldReturn404WhenInvalidResource() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasksABC"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldBeEqualAndCode200() throws IOException, InterruptedException { + manager.addTask(task); + + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasks/1"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode()); + assertEquals(task, gson.fromJson(response.body(), Task.class)); + } + + @Test + public void shouldSameAmountAndCode200() throws IOException, InterruptedException { + manager.addTask(task); + manager.addTask(new Task("task2", "demo", Status.NEW)); + + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasks"), + HttpResponse.BodyHandlers.ofString()); + + List list = gson.fromJson(response.body(), new ListSubTaskTypeToken().getType()); + + assertEquals(200, response.statusCode()); + assertEquals(2, list.size()); + } + + @Test + public void shouldReturn404WhenNoId() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasks/1"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn404WhenSubResourcePresent() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasks/1/sub"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn404WhenIdInvalid() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasks/-2a"), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(400, response.statusCode()); + } + + } + + @Nested + class TaskPostTest { + + @Test + public void shouldBeEqualAndCode201() throws IOException, InterruptedException { + String taskJson = gson.toJson(task); + + HttpResponse response = client.send( + getPost("http://localhost:8080/tasks", taskJson), + HttpResponse.BodyHandlers.ofString()); + + task.setTaskId(1); + + assertEquals(201, response.statusCode()); + assertEquals(task, manager.getTask(1)); + } + + @Test + public void shouldBeUpdatedAndCode201() throws IOException, InterruptedException { + manager.addTask(task); + + task.setTitle("new title"); + String taskJson = gson.toJson(task); + + HttpResponse response = client.send( + getPost("http://localhost:8080/tasks", taskJson), + HttpResponse.BodyHandlers.ofString()); + + + assertEquals(201, response.statusCode()); + assertEquals(task, manager.getTask(1)); + } + + @Test + public void shouldNotBeUpdatedAndCode404WhenNotFound() throws IOException, InterruptedException { + + task.setTaskId(1); + String taskJson = gson.toJson(task); + + HttpResponse response = client.send( + getPost("http://localhost:8080/tasks", taskJson), + HttpResponse.BodyHandlers.ofString()); + + + assertEquals(404, response.statusCode()); + } + + @Test + public void shouldReturn405AndNotPostWhenIdIsPresent() throws IOException, InterruptedException { + String taskJson = gson.toJson(task); + + HttpResponse response = client.send( + getPost("http://localhost:8080/tasks/1", taskJson), + HttpResponse.BodyHandlers.ofString() + ); + task.setTaskId(1); + + assertEquals(405, response.statusCode()); + assertNull(manager.getTask(1)); + } + + @Test + public void shouldReturn400WhenNoBody() throws IOException, InterruptedException { + + HttpResponse response = client.send( + getPost("http://localhost:8080/tasks", ""), + HttpResponse.BodyHandlers.ofString() + ); + task.setTaskId(1); + + assertEquals(400, response.statusCode()); + assertNull(manager.getTask(1)); + } + + @Test + public void shouldReturn400WhenInvalidBody() throws IOException, InterruptedException { + String taskJson = "{blablabla:blabla}"; + + HttpResponse response = client.send( + getPost("http://localhost:8080/tasks", taskJson), + HttpResponse.BodyHandlers.ofString() + ); + task.setTaskId(1); + + assertEquals(400, response.statusCode()); + assertNull(manager.getTask(1)); + } + + @Test + public void shouldReturn406WhenTimeOverlap() throws IOException, InterruptedException { + manager.addTask(new Task("task1", "demo", Status.NEW, 10, epochTime)); + + String taskJson = gson.toJson(new Task("task2", "demo", Status.NEW, 5, epochTime)); + + HttpResponse response = client.send( + getPost("http://localhost:8080/tasks", taskJson), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(406, response.statusCode()); + } + } + + @Nested + class TaskDeleteTest { + + private HttpRequest getRequest(String value) { + return getMethod("DELETE", value); + } + + @Test + public void shouldReturn200AndDeleteAllTasks() throws IOException, InterruptedException { + manager.addTask(task); + manager.addTask(new Task("task2", "demo", Status.NEW)); + + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasks"), + HttpResponse.BodyHandlers.ofString() + ); + + assertEquals(200, response.statusCode()); + assertEquals(0, manager.getTasks().size()); + } + + @Test + public void shouldReturn200AndDeleteTask() throws IOException, InterruptedException { + manager.addTask(task); + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasks/1"), + HttpResponse.BodyHandlers.ofString() + ); + + assertEquals(200, response.statusCode()); + assertNull(manager.getTask(1)); + } + } + + @Nested + class TaskOptionsTest { + + private HttpRequest getRequest(String value) { + return getMethod("OPTIONS", value); + } + + @Test + public void shouldReturn204AndValidMethods() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasks"), + HttpResponse.BodyHandlers.ofString() + ); + List expected = List.of("GET, POST, DELETE, OPTIONS, HEAD"); + + List methods = response.headers().allValues("Allow"); + + assertEquals(204, response.statusCode()); + assertEquals(expected, methods); + } + + @Test + public void shouldReturn204AndValidMethodsWhenIdIsPresent() throws IOException, InterruptedException { + HttpResponse response = client.send( + getRequest("http://localhost:8080/tasks/1"), + HttpResponse.BodyHandlers.ofString() + ); + List expected = List.of("GET, DELETE, OPTIONS, HEAD"); + + List methods = response.headers().allValues("Allow"); + + assertEquals(204, response.statusCode()); + assertEquals(expected, methods); + } + + } +} \ No newline at end of file From 7d399da42207d07d8f43d29816fcf4b7f8f2409c Mon Sep 17 00:00:00 2001 From: Crodi Date: Tue, 23 Sep 2025 21:47:05 +0300 Subject: [PATCH 13/22] updating iml adding gson --- .idea/libraries/gson_2_9_0.xml | 9 +++++++++ java-kanban.iml | 1 + 2 files changed, 10 insertions(+) create mode 100644 .idea/libraries/gson_2_9_0.xml diff --git a/.idea/libraries/gson_2_9_0.xml b/.idea/libraries/gson_2_9_0.xml new file mode 100644 index 0000000..2377008 --- /dev/null +++ b/.idea/libraries/gson_2_9_0.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/java-kanban.iml b/java-kanban.iml index 66c15d2..48a7097 100644 --- a/java-kanban.iml +++ b/java-kanban.iml @@ -24,5 +24,6 @@ + \ No newline at end of file From 58574f3b44f549e52f5775b384cd0ee70d5dd629 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 24 Sep 2025 01:10:13 +0300 Subject: [PATCH 14/22] fix: update* changed logic docs: add documentation for update* methods --- src/managers/InMemoryTaskManager.java | 91 +++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 6 deletions(-) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index edfbbfd..26984a8 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -138,28 +138,73 @@ public List getPrioritizedTasks() { return taskTimeController.getPrioritizedTasks(); } + /** + * Обновляет существующую задачу (Task) новыми данными. + * + *

Обновляемые поля:

+ *
    + *
  • Заголовок (title)
  • + *
  • Описание (description)
  • + *
  • Статус (status)
  • + *
  • Продолжительность (duration)
  • + *
  • Время начала (startTime)
  • + *
+ * + *

Временная логика:

+ *
    + *
  • Старая задача временно удаляется из временного контроллера
  • + *
  • Проверяется пересечение временных интервалов для новой версии задачи
  • + *
  • Обновленная задача добавляется обратно во временной контроллер
  • + *
  • Если найдено пересечение времени старая задача добавляется обратно во временной контроллер
  • + *
+ * + * @param task задача с обновленными данными (должна содержать корректный taskId) + * @throws TaskNotFound если задача с указанным Id не найдена + * @throws TaskTimeOverlapException если новое время задачи пересекается с существующими задачами + */ @Override public void updateTask(Task task) { int id = task.getTaskId(); if (!tasks.containsKey(id)) { throw new TaskNotFound("Task with id: " + id + " not found"); } + Task oldTask = tasks.get(id); + taskTimeController.remove(oldTask); + if (taskTimeController.isTimeOverlapping(task)) { + taskTimeController.add(oldTask); throw new TaskTimeOverlapException("Time overlap! Can`t add task: " + task); } - Task oldTask = tasks.get(id); - taskTimeController.remove(oldTask); - oldTask.setTitle(task.getTitle()); oldTask.setDescription(task.getDescription()); oldTask.setStatus(task.getStatus()); oldTask.setDuration(task.getDuration()); oldTask.setStartTime(task.getStartTime()); - taskTimeController.add(task); + taskTimeController.add(oldTask); } + /** + * Обновляет существующий эпик (Epic) новыми данными. + *

Метод обновляет только основные данные эпика. + *

В отличие от обычных задач, для эпиков: + * + *

    + *
  • Не проверяются временные интервалы
  • + *
  • Не обновляется статус (статус эпика рассчитывается автоматически на основе подзадач)
  • + *
  • Не обновляются временные характеристики (duration, startTime)
  • + *
+ * + *

Обновляемые поля:

+ *
    + *
  • Заголовок (title)
  • + *
  • Описание (description)
  • + *
+ * + * @param epic эпик с обновленными данными (должен содержать корректный taskId) + * @throws TaskNotFound если эпик с указанным Id не найдена + */ @Override public void updateEpic(Epic epic) { int id = epic.getTaskId(); @@ -172,17 +217,51 @@ public void updateEpic(Epic epic) { oldEpic.setDescription(epic.getDescription()); } + /** + * Обновляет существующую подзадачу (SubTask) новыми данными. + * + *

Обновляемые поля:

+ *
    + *
  • Заголовок (title)
  • + *
  • Описание (description)
  • + *
  • Статус (status)
  • + *
  • Продолжительность (duration)
  • + *
  • Время начала (startTime)
  • + *
+ * + *

Вторичные эффекты:

+ *
    + *
  • Автоматическое обновление статуса родительского эпика
  • + *
  • Пересчет временных характеристик родительского эпика
  • + *
+ * + *

Временная логика:

+ *
    + *
  • Старая подзадача временно удаляется из временного контроллера
  • + *
  • Проверяется пересечение временных интервалов для новой версии задачи
  • + *
  • Обновленная подзадача добавляется обратно во временной контроллер
  • + *
  • Если найдено пересечение времени старая подзадача добавляется обратно во временной контроллер
  • + *
+ * + * @param subTask подзадача с обновленными данными (должна содержать корректный taskId) + * @throws TaskNotFound если подзадача с указанным Id не найдена + * @throws TaskTimeOverlapException если новое время подзадачи пересекается с существующими задачами + * @see #updateEpicStatus(Epic) + * @see TaskTimeController#isTimeOverlapping(Task) + */ @Override public void updateSubTask(SubTask subTask) { int id = subTask.getTaskId(); if (!subtasks.containsKey(id)) { throw new TaskNotFound("SubTask with id: " + id + " not found"); } + SubTask oldSubTask = subtasks.get(id); + taskTimeController.remove(oldSubTask); + if (taskTimeController.isTimeOverlapping(subTask)) { + taskTimeController.add(oldSubTask); throw new TaskTimeOverlapException("Time overlap! Can`t add subtask: " + subTask); } - SubTask oldSubTask = subtasks.get(id); - taskTimeController.remove(oldSubTask); oldSubTask.setTitle(subTask.getTitle()); oldSubTask.setDescription(subTask.getDescription()); From 68a1b44ae1a9bbbd29c160b7b232bf38114e6d40 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 24 Sep 2025 01:12:28 +0300 Subject: [PATCH 15/22] fix: deleted JsonSyntaxException catching from handlers, now only BaseHttpHandler catches it docs: add documentation for complicated classes and methods --- src/http/HttpTaskServer.java | 15 +-- src/http/handlers/BaseHttpHandler.java | 91 ++++++++++++- src/http/handlers/EpicsHandler.java | 144 ++++++++++++++++++++- src/http/handlers/HistoryHandler.java | 101 +++++++++++++++ src/http/handlers/PrioritizedHandler.java | 70 ++++++++++ src/http/handlers/SubTasksHandler.java | 124 +++++++++++++++++- src/http/handlers/TaskHandler.java | 148 +++++++++++++++++++++- src/util/http/RequestSegments.java | 60 ++++++++- 8 files changed, 713 insertions(+), 40 deletions(-) diff --git a/src/http/HttpTaskServer.java b/src/http/HttpTaskServer.java index 3e8321f..c997b3a 100644 --- a/src/http/HttpTaskServer.java +++ b/src/http/HttpTaskServer.java @@ -4,18 +4,16 @@ import com.google.gson.GsonBuilder; import com.sun.net.httpserver.HttpServer; import http.handlers.*; +import managers.InMemoryTaskManager; import managers.TaskManager; -import managers.filedbacked.FileBackedTaskManager; import util.gsonadapters.DurationAdapter; import util.gsonadapters.LocalDateTimeAdapter; import util.http.JsonBuilder; -import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.time.Duration; import java.time.LocalDateTime; -import java.util.Scanner; public class HttpTaskServer { private final int PORT = 8080; @@ -26,16 +24,7 @@ public class HttpTaskServer { private final JsonBuilder jsonBuilder; public static void main(String[] args) { - Scanner scanner = new Scanner(System.in); - String cmd = ""; - HttpTaskServer server = new HttpTaskServer(new FileBackedTaskManager(new File("resources/httpTasks.csv"))); - server.start(); - - System.out.println("Commands to stop: c, stop"); - while (!cmd.equals("c") && !cmd.equals("stop")) { - cmd = scanner.next(); - } - server.stop(); + new HttpTaskServer(new InMemoryTaskManager()).start(); } public HttpTaskServer(TaskManager manager) { diff --git a/src/http/handlers/BaseHttpHandler.java b/src/http/handlers/BaseHttpHandler.java index faf8997..c474243 100644 --- a/src/http/handlers/BaseHttpHandler.java +++ b/src/http/handlers/BaseHttpHandler.java @@ -15,6 +15,40 @@ import static util.enums.Endpoint.*; import static util.http.RequestSegments.getRequestSegments; +/** + * Абстрактный базовый класс для обработки HTTP-запросов. + *

+ * Предоставляет общую функциональность для обработки HTTP-запросов, включая: + *

+ *
    + *
  • Парсинг и валидацию сегментов URI
  • + *
  • Маршрутизацию запросов по HTTP-методам
  • + *
  • Стандартизированную отправку ответов
  • + *
  • Обработку ошибок и исключений
  • + *
+ * + *

Жизненный цикл обработки запроса:

+ *
    + *
  1. Парсинг сегментов пути из URI
  2. + *
  3. Валидация сегментов и HTTP-метода
  4. + *
  5. Проверка ресурсов (абстрактный метод)
  6. + *
  7. Маршрутизация на соответствующий обработчик метода
  8. + *
  9. Обработка исключений и отправка ошибок
  10. + *
+ * + *

Поддерживаемые HTTP-методы: GET, POST, DELETE, OPTIONS, HEAD

+ * + *

Особенности обработки:

+ *
    + *
  • Метод HEAD обрабатывается как GET, но без отправки тела ответа
  • + *
  • OPTIONS возвращает разрешенные методы для ресурса
  • + *
  • Некорректные запросы возвращают соответствующие HTTP-статусы
  • + *
  • Все ответы отправляются в формате JSON с UTF-8 кодировкой
  • + *
+ * + *

Наследование: Классы-наследники должны реализовать абстрактные методы + * для конкретной логики обработки ресурсов.

+ */ public abstract class BaseHttpHandler implements HttpHandler { private final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; protected final TaskManager manager; @@ -22,7 +56,6 @@ public abstract class BaseHttpHandler implements HttpHandler { protected Gson gson; protected JsonBuilder jsonBuilder; - public BaseHttpHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) { this.manager = manager; this.gson = gson; @@ -42,10 +75,7 @@ public void handle(HttpExchange exchange) throws IOException { mapEndpoints(exchange, segments); } catch (JsonSyntaxException e) { - sendText(exchange, jsonBuilder.badRequest( - "Json must contain: taskId, title, description and status[NEW, IN_PROGRESS, DONE]. " + - "If subtasks is resource: epicId. " + - "If being added taskId must be 0."), 400); + sendText(exchange, jsonBuilder.badRequest("Invalid Json"), 400); } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { @@ -53,11 +83,19 @@ public void handle(HttpExchange exchange) throws IOException { sendServerError(exchange); e.printStackTrace(); } catch (IOException ex) { - System.out.println("Failed to send response"); + System.err.println("Failed to send response"); + ex.printStackTrace(); } } } + /** + * Подготавливает и валидирует сегменты запроса. + * + * @param exchange HTTP-обмен для анализа + * @return подготовленные сегменты запроса или {@code null} если валидация не пройдена + * @throws IOException если возникает ошибка при обработке запроса + */ private RequestSegments getPreparedSegments(HttpExchange exchange) throws IOException { RequestSegments segments = getRequestSegments(exchange); @@ -70,6 +108,13 @@ private RequestSegments getPreparedSegments(HttpExchange exchange) throws IOExce return segments; } + /** + * Маршрутизирует запрос на соответствующий обработчик метода. + * + * @param exchange HTTP-обмен для обработки + * @param segments разобранные сегменты запроса + * @throws IOException если возникает ошибка при обработке запроса + */ private void mapEndpoints(HttpExchange exchange, RequestSegments segments) throws IOException { switch (segments.endpoint()) { case OPTIONS -> handleOptions(exchange, segments); @@ -80,8 +125,26 @@ private void mapEndpoints(HttpExchange exchange, RequestSegments segments) throw } } + /** + * Проверяет валидность сегментов запроса. + * Отправляет ответ, если проверка не пройдена. + *

+ * Выполняет следующие проверки: + *

+ *
    + *
  • Поддержка HTTP-метода
  • + *
  • Корректность endpoint
  • + *
  • Валидность структуры подресурсов
  • + *
  • Корректность числового идентификатора
  • + *
  • Разрешение подресурсов только для эпиков
  • + *
+ * + * @param exchange HTTP-обмен + * @param segments сегменты для валидации + * @return {@code true} если сегменты валидны, иначе {@code false} + * @throws IOException если возникает ошибка при отправке ответа об ошибке + */ private boolean isSegmentsValid(HttpExchange exchange, RequestSegments segments) throws IOException { - System.out.printf("Validating segments: %s Method: %s\n", segments, exchange.getRequestMethod()); if (segments.endpoint() == INVALID_METHOD) { exchange.getResponseHeaders().add("Allow", "GET, POST, DELETE, HEAD, OPTIONS"); @@ -138,6 +201,20 @@ protected void sendServerError(HttpExchange exchange) throws IOException { exchange.close(); } + /** + * Проверяет запрет метода POST для ресурсов с идентификатором. + * Отправляет ответ, если метод запрещен + *

+ * POST запросы разрешены только для формата пути /resource. + * Для форматов /resource/id и /resource/id/subresource + * ресурсов разрешены только GET, DELETE, OPTIONS, HEAD. + *

+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @return true если операция POST запрещена, false если разрешена + * @throws IOException если возникает ошибка при отправке ответа об ошибке + */ protected boolean isPostForbidden(HttpExchange exchange, RequestSegments segments) throws IOException { if (segments.id() != 0) { exchange.getResponseHeaders().add("Allow", "GET, DELETE, OPTIONS, HEAD"); diff --git a/src/http/handlers/EpicsHandler.java b/src/http/handlers/EpicsHandler.java index 9c06d6a..fecf388 100644 --- a/src/http/handlers/EpicsHandler.java +++ b/src/http/handlers/EpicsHandler.java @@ -13,13 +13,72 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.time.format.DateTimeParseException; +/** + * Обработчик HTTP-запросов для управления эпиками (Epic) и их подзадачами. + *

+ * Расширяет функциональность {@link TaskHandler} для работы с эпиками. + * Обеспечивает REST API для операций CRUD над эпиками и их подзадачами. + *

+ * + *

Поддерживаемые эндпоинты для эпиков:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + *
МетодПутьДействие
HEADлюбой путьПолучить заголовки
GET/epicsПолучить все эпики
GET/epics/{id}Получить эпик по Id
GET/epics/{id}/subtasksПолучить все подзадачи эпика
POST/epicsСоздать новый эпик
POST/epics/{id}Запрещено - метод не разрешен
POST/epics/{id}/subtasksЗапрещено - метод не разрешен
DELETE/epicsУдалить все эпики
DELETE/epics/{id}Удалить эпик по Id
DELETE/epics/{id}/subtasksУдалить все подзадачи эпика
OPTIONSлюбой путьПолучить разрешенные методы
+ * + *

Формат JSON для эпика:

+ *
+ * {
+ *   "taskId": 0,           // 0 для создания, >0 для обновления
+ *   "title": "string",     // обязательное поле
+ *   "description": "string", // обязательное поле
+ *   "status": "NEW|IN_PROGRESS|DONE", // (только для чтения)
+ *   "subtasks": [1, 2, 3], // (только для чтения)
+ *   "duration": 10,        // (только для чтения)
+ *   "startTime": "1970-01-01T00:00:00.000" //(только для чтения)
+ * }
+ * 
+ * + *

Обработка ошибок:

+ *
    + *
  • 400 - Неверный формат JSON или отсутствуют обязательные поля
  • + *
  • 404 - Эпик не найден или неверный путь
  • + *
  • 405 - Метод не разрешен для ресурса
  • + *
  • 500 - Внутренняя ошибка сервера
  • + *
+ */ public class EpicsHandler extends TaskHandler { public EpicsHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) { super(manager, gson, jsonBuilder); } + /** + * Обрабатывает HTTP-метод OPTIONS для эпиков. + *

+ * Возвращает разрешенные методы в зависимости от контекста: + *

+ *
    + *
  • Для /epics: GET, POST, DELETE, OPTIONS, HEAD
  • + *
  • Для /epics/{id} и /epics/{id}/subtasks: GET, DELETE, OPTIONS, HEAD
  • + *
+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + */ @Override protected void handleOptions(HttpExchange exchange, RequestSegments segments) throws IOException { if (segments.id() == 0 && segments.subResource().isEmpty()) { @@ -30,6 +89,21 @@ protected void handleOptions(HttpExchange exchange, RequestSegments segments) th exchange.sendResponseHeaders(204, -1); } + /** + * Обрабатывает HTTP-метод GET для эпиков. + *

+ * В зависимости от пути запроса: + *

+ *
    + *
  • /epics - возвращает список всех эпиков
  • + *
  • /epics/{id} - возвращает конкретный эпик
  • + *
  • /epics/{id}/subtasks - возвращает все подзадачи указанного эпика
  • + *
+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + */ @Override protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException { int id = segments.id(); @@ -52,6 +126,20 @@ protected void handleGet(HttpExchange exchange, RequestSegments segments) throws } } + /** + * Обрабатывает HTTP-метод POST для эпиков. + *

+ * Создает новый эпик или обновляет существующий: + *

+ *
    + *
  • taskId = 0: создает новый эпик через конструктор
  • + *
  • taskId > 0: обновляет существующий эпик
  • + *
+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + */ @Override protected void handlePost(HttpExchange exchange, RequestSegments segments) throws IOException { if (isPostForbidden(exchange, segments)) { @@ -74,16 +162,30 @@ protected void handlePost(HttpExchange exchange, RequestSegments segments) throw manager.updateEpic(epic); } sendText(exchange, gson.toJson(manager.getEpicWithoutHistory(epic.getTaskId())), 201); - } catch (JsonSyntaxException e) { - sendText(exchange, - jsonBuilder.badRequest( - "Json must contain: taskId, title, description and status[NEW, IN_PROGRESS, DONE]."), - 400); + } catch (DateTimeParseException e) { + sendText(exchange, jsonBuilder.badRequest("Invalid date format"), 400); } catch (TaskNotFound e) { sendNotFound(exchange, jsonBuilder.notFound(e.getMessage())); } } + /** + * Обрабатывает HTTP-метод DELETE для эпиков. + *

+ * В зависимости от пути запроса: + *

+ *
    + *
  • /epics - удаляет все эпики
  • + *
  • /epics/{id} - удаляет конкретный эпик
  • + *
  • /epics/{id}/subtasks - удаляет все подзадачи указанного эпика
  • + *
+ * + *

Удаление эпика автоматически удаляет все его подзадачи.

+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + */ @Override protected void handleDelete(HttpExchange exchange, RequestSegments segments) throws IOException { @@ -105,6 +207,22 @@ protected void handleDelete(HttpExchange exchange, RequestSegments segments) thr } } + /** + * Проверяет корректность ресурса и подресурсов в запросе. + *

+ * Выполняет следующие проверки: + *

+ *
    + *
  • Убеждается, что запрос адресован эпикам ("epics")
  • + *
  • Проверяет валидность подресурса (только "subtasks")
  • + *
  • Запрещает POST запросы для подресурсов
  • + *
+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @return true если ресурс неверен или запрос запрещен, иначе false + * @throws IOException если возникает ошибка при отправке ответа об ошибке + */ @Override protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException { if (!"epics".equals(segments.resource())) { @@ -126,6 +244,22 @@ protected boolean validateResources(HttpExchange exchange, RequestSegments segme return false; } + /** + * Проверяет валидность объекта эпика. + *

+ * Проверяет наличие обязательных полей: + *

+ *
    + *
  • title - заголовок эпика
  • + *
  • description - описание эпика
  • + *
+ * + *

Отличие от TaskHandler: для эпиков статус не является обязательным полем, + * так как он рассчитывается автоматически на основе подзадач.

+ * + * @param task эпик для валидации + * @throws JsonSyntaxException если отсутствуют обязательные поля + */ @Override protected void isTaskValid(Task task) { if (task.getTitle() == null || task.getDescription() == null) { diff --git a/src/http/handlers/HistoryHandler.java b/src/http/handlers/HistoryHandler.java index ff62596..876980b 100644 --- a/src/http/handlers/HistoryHandler.java +++ b/src/http/handlers/HistoryHandler.java @@ -8,35 +8,136 @@ import java.io.IOException; +/** + * Обработчик HTTP-запросов для получения истории просмотров задач. + *

+ * Предоставляет доступ к истории последних просмотренных задач. + *

+ * + *

Поддерживаемые эндпоинты:

+ * + * + * + * + * + * + * + * + *
МетодПутьДействие
HEAD/historyПолучить заголовки
GET/historyПолучить историю просмотров
OPTIONS/historyПолучить разрешенные методы
POST/historyЗапрещено - метод не разрешен
DELETE/historyЗапрещено - метод не разрешен
GET/history/{id}Запрещено - метод не разрешен
+ * + *

Формат ответа:

+ *
+ * [
+ *   {
+ *     "taskId": 1,
+ *     "title": "Название задачи",
+ *     "description": "Описание задачи",
+ *     "status": "NEW",
+ *     "type": "TASK|EPIC|SUBTASK"
+ *     // ... другие поля задачи в зависимости от типа
+ *   },
+ *   {
+ *     "taskId": 2,
+ *     "title": "Другая задача",
+ *     // ...
+ *   }
+ * ]
+ * 
+ * + *

Обработка ошибок:

+ *
    + *
  • 404 - Неверный путь
  • + *
  • 405 - Метод не разрешен для ресурса
  • + *
  • 500 - Внутренняя ошибка сервера
  • + *
+ */ public class HistoryHandler extends BaseHttpHandler { public HistoryHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) { super(manager, gson, jsonBuilder); } + /** + * Обрабатывает HTTP-метод OPTIONS для истории. + *

+ * Возвращает разрешенные методы для ресурса истории: + * GET, OPTIONS, HEAD + *

+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + */ @Override protected void handleOptions(HttpExchange exchange, RequestSegments segments) throws IOException { exchange.getResponseHeaders().add("Allow", "GET, OPTIONS, HEAD"); exchange.sendResponseHeaders(204, -1); } + /** + * Обрабатывает HTTP-метод GET для истории. + *

+ * Возвращает полную историю просмотров задач в формате JSON. + *

+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + */ @Override protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException { sendText(exchange, gson.toJson(manager.getHistory()), 200); } + /** + * Обрабатывает HTTP-метод POST для истории или списка приоритетов. + *

+ * Ресурс только для чтения, поэтому POST запросы запрещены. + * Возвращает статус 405 Method Not Allowed с заголовком Allow. + *

+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + */ @Override protected void handlePost(HttpExchange exchange, RequestSegments segments) throws IOException { exchange.getResponseHeaders().add("Allow", "GET, HEAD, OPTIONS"); sendMethodNotAllowed(exchange); } + /** + * Обрабатывает HTTP-метод DELETE для истории или списка приоритетов. + *

+ * Ресурс только для чтения, поэтому DELETE запросы запрещены. + * Возвращает статус 405 Method Not Allowed с заголовком Allow. + *

+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + */ @Override protected void handleDelete(HttpExchange exchange, RequestSegments segments) throws IOException { exchange.getResponseHeaders().add("Allow", "GET, HEAD, OPTIONS"); sendMethodNotAllowed(exchange); } + /** + * Проверяет корректность ресурса и пути запроса для истории. + *

+ * Выполняет следующие проверки: + *

+ *
    + *
  • Убеждается, что запрос адресован истории ("history")
  • + *
  • Проверяет, что в пути нет идентификатора и подресурсов
  • + *
+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @return true если путь неверен, иначе false + * @throws IOException если возникает ошибка при отправке ответа 404 + */ @Override protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException { if (!"history".equals(segments.resource())) { diff --git a/src/http/handlers/PrioritizedHandler.java b/src/http/handlers/PrioritizedHandler.java index dd2a991..4b7e85d 100644 --- a/src/http/handlers/PrioritizedHandler.java +++ b/src/http/handlers/PrioritizedHandler.java @@ -8,17 +8,87 @@ import java.io.IOException; +/** + * Обработчик HTTP-запросов для получения отсортированного списка задач по приоритету. + *

+ * Расширяет функциональность {@link HistoryHandler} для обработки запрещенных методов (POST, DELETE). + *

+ * + *

Поддерживаемые эндпоинты:

+ * + * + * + * + * + * + * + * + *
МетодПутьДействие
HEAD/prioritizedПолучить заголовки
GET/prioritizedПолучить отсортированный список задач
OPTIONS/prioritizedПолучить разрешенные методы
POST/prioritizedЗапрещено - метод не разрешен
DELETE/prioritizedЗапрещено - метод не разрешен
GET/prioritized/{id}Запрещено - метод не разрешен
+ * + *

Формат ответа:

+ *
+ * [
+ *   {
+ *     "taskId": 3,
+ *     "title": "Ранняя задача",
+ *     "startTime": "1970-01-01T00:00:00.000",
+ *     "duration": 10,
+ *     // ... другие поля задачи
+ *   },
+ *   {
+ *     "taskId": 1,
+ *     "title": "Задача с временем",
+ *     "startTime": "2000-01-01T00:00:00.000",
+ *     "duration": 10,
+ *     // ...
+ *   }
+ * ]
+ * 
+ * + *

Обработка ошибок:

+ *
    + *
  • 404 - Неверный путь
  • + *
  • 405 - Метод не разрешен для ресурса
  • + *
  • 500 - Внутренняя ошибка сервера
  • + *
+ */ public class PrioritizedHandler extends HistoryHandler { public PrioritizedHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) { super(manager, gson, jsonBuilder); } + /** + * Обрабатывает HTTP-метод GET для приоритизированного списка. + *

+ * Возвращает задачи, отсортированные по времени начала (startTime). + *

+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + * @see util.TaskTimeController#getPrioritizedTasks() + */ @Override protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException { sendText(exchange, gson.toJson(manager.getPrioritizedTasks()), 200); } + /** + * Проверяет корректность ресурса и пути запроса для приоритизированного списка. + *

+ * Выполняет следующие проверки: + *

+ *
    + *
  • Убеждается, что запрос адресован приоритизированному списку ("prioritized")
  • + *
  • Проверяет, что в пути нет идентификатора и подресурсов
  • + *
+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @return true если путь неверен, иначе false + * @throws IOException если возникает ошибка при отправке ответа 404 + */ @Override protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException { if (!"prioritized".equals(segments.resource())) { diff --git a/src/http/handlers/SubTasksHandler.java b/src/http/handlers/SubTasksHandler.java index 392ebd1..6ed8329 100644 --- a/src/http/handlers/SubTasksHandler.java +++ b/src/http/handlers/SubTasksHandler.java @@ -13,13 +13,69 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.time.format.DateTimeParseException; +/** + * Обработчик HTTP-запросов для управления подзадачами (SubTask). + *

+ * Расширяет функциональность {@link TaskHandler} для работы с подзадачами. + * Обеспечивает REST API для операций CRUD над подзадачами. + *

+ * + *

Поддерживаемые эндпоинты для подзадач:

+ * + * + * + * + * + * + * + * + * + *
МетодПутьДействие
HEAD/subtasks или /subtasks/{id}Получить заголовки
GET/subtasksПолучить все подзадачи
GET/subtasks/{id}Получить подзадачу по Id
POST/subtasksСоздать новую подзадачу
POST/subtasks/{id}Запрещено - метод не разрешен
DELETE/subtasksУдалить все подзадачи
DELETE/subtasks/{id}Удалить подзадачу по Id
+ * + *

Формат JSON для подзадачи:

+ *
+ * {
+ *   "taskId": 0,           // 0 для создания, >0 для обновления
+ *   "title": "string",     // обязательное поле
+ *   "description": "string", // обязательное поле
+ *   "status": "NEW|IN_PROGRESS|DONE", // обязательное поле
+ *   "epicId": 123,         // обязательное поле - ID родительского эпика
+ *   "duration": 10,        // продолжительность в минутах (опционально)
+ *   "startTime": "1970-01-01T00:00:00.000" // дата и время ISO (опционально)
+ * }
+ * 
+ * + *

Обработка ошибок:

+ *
    + *
  • 400 - Неверный формат JSON или отсутствуют обязательные поля
  • + *
  • 404 - Подзадача или родительский эпик не найдены или неверный путь
  • + *
  • 405 - Метод не разрешен для ресурса
  • + *
  • 406 - Пересечение временных интервалов с существующими задачами
  • + *
  • 500 - Внутренняя ошибка сервера
  • + *
+ */ public class SubTasksHandler extends TaskHandler { public SubTasksHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) { super(manager, gson, jsonBuilder); } + /** + * Обрабатывает HTTP-метод GET для подзадач. + *

+ * В зависимости от наличия идентификатора: + *

+ *
    + *
  • Без ID: возвращает список всех подзадач
  • + *
  • С ID: возвращает конкретную подзадачу
  • + *
+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + */ @Override protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException { if (segments.id() == 0) { @@ -34,6 +90,22 @@ protected void handleGet(HttpExchange exchange, RequestSegments segments) throws } } + /** + * Обрабатывает HTTP-метод POST для подзадач. + *

+ * Создает новую подзадачу или обновляет существующую в зависимости от taskId: + *

+ *
    + *
  • taskId = 0: создание новой подзадачи
  • + *
  • taskId > 0: обновление существующей подзадачи
  • + *
+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + * @throws TaskNotFound если родительский эпик или подзадача для обновления не существует + * @throws TaskTimeOverlapException если обнаружено пересечение временных интервалов + */ @Override protected void handlePost(HttpExchange exchange, RequestSegments segments) throws IOException { if (isPostForbidden(exchange, segments)) { @@ -55,20 +127,32 @@ protected void handlePost(HttpExchange exchange, RequestSegments segments) throw manager.updateSubTask(subTask); } sendText(exchange, gson.toJson(manager.getSubTaskWithoutHistory(subTask.getTaskId())), 201); - } catch (JsonSyntaxException e) { - sendText(exchange, - jsonBuilder.badRequest( - "Json must contain: taskId, title, description, status," + - " status[NEW, IN_PROGRESS, DONE], epicId."), - 400); } catch (TaskNotFound e) { sendNotFound(exchange, jsonBuilder.notFound(e.getMessage())); + } catch (DateTimeParseException e) { + sendText(exchange, jsonBuilder.badRequest("Invalid date format"), 400); } catch (TaskTimeOverlapException e) { sendHasOverlaps(exchange, jsonBuilder.hasOverlaps()); } } + /** + * Обрабатывает HTTP-метод DELETE для подзадач. + *

+ * В зависимости от наличия идентификатора: + *

+ *
    + *
  • Без ID: удаляет все подзадачи
  • + *
  • С ID: удаляет конкретную подзадачу
  • + *
+ * + *

Удаление подзадачи автоматически обновляет статус и поля времени родительского эпика.

+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + */ @Override protected void handleDelete(HttpExchange exchange, RequestSegments segments) throws IOException { if (segments.id() == 0) { @@ -80,6 +164,17 @@ protected void handleDelete(HttpExchange exchange, RequestSegments segments) thr } } + /** + * Проверяет корректность ресурса в запросе. + *

+ * Убеждается, что запрос адресован именно подзадачам ("subtasks"). + *

+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @return true если ресурс неверен (не "subtasks"), иначе false + * @throws IOException если возникает ошибка при отправке ответа 404 + */ @Override protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException { if (!"subtasks".equals(segments.resource())) { @@ -89,6 +184,23 @@ protected boolean validateResources(HttpExchange exchange, RequestSegments segme return false; } + /** + * Проверяет валидность объекта подзадачи. + *

+ * Проверяет наличие обязательных полей для подзадачи: + *

+ *
    + *
  • title - заголовок подзадачи
  • + *
  • description - описание подзадачи
  • + *
  • status - статус подзадачи
  • + *
  • epicId - идентификатор родительского эпика (должен быть > 0)
  • + *
+ * + *

Отличие от TaskHandler: для подзадач дополнительно проверяется наличие epicId.

+ * + * @param task подзадача для валидации (приводится к SubTask) + * @throws JsonSyntaxException если отсутствуют обязательные поля или epicId = 0 + */ @Override protected void isTaskValid(Task task) { SubTask subTask = (SubTask) task; diff --git a/src/http/handlers/TaskHandler.java b/src/http/handlers/TaskHandler.java index ffcb6fd..84d011c 100644 --- a/src/http/handlers/TaskHandler.java +++ b/src/http/handlers/TaskHandler.java @@ -12,13 +12,76 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.time.format.DateTimeParseException; +/** + * Обработчик HTTP-запросов для управления задачами (Task). + *

+ * Обеспечивает REST API для операций CRUD над задачами: + *

+ *
    + *
  • Создание новых задач
  • + *
  • Получение задач (всех или по идентификатору)
  • + *
  • Обновление существующих задач
  • + *
  • Удаление задач (всех или по идентификатору)
  • + *
+ * + *

Поддерживаемые эндпоинты:

+ * + * + * + * + * + * + * + * + * + * + *
МетодПутьДействие
HEAD/tasks или /tasks/{id}Получить заголовки
GET/tasksПолучить все задачи
GET/tasks/{id}Получить задачу по Id
POST/tasksСоздать новую задачу
POST/tasks/{id}Запрещено - метод не разрешен
DELETE/tasksУдалить все задачи
DELETE/tasks/{id}Удалить задачу по Id
OPTIONS/tasks или /tasks/{id}Получить разрешенные методы
+ * + *

Формат JSON для задачи:

+ *
+ * {
+ *   "taskId": 0,           // 0 для создания, >0 для обновления
+ *   "title": "string",     // обязательное поле
+ *   "description": "string", // обязательное поле
+ *   "status": "NEW|IN_PROGRESS|DONE", // обязательное поле
+ *   "duration": 10,        // продолжительность в минутах (опционально)
+ *   "startTime": "1970-01-01T00:00:00.000" // дата и время ISO (опционально)
+ * }
+ * 
+ * + *

Обработка ошибок:

+ *
    + *
  • 400 - Неверный формат JSON или отсутствуют обязательные поля
  • + *
  • 404 - Задача не найдена или неверный путь
  • + *
  • 405 - Метод не разрешен для ресурса
  • + *
  • 406 - Пересечение временных интервалов с существующими задачами
  • + *
  • 500 - Внутренняя ошибка сервера
  • + *
+ * + * @see java.time.format.DateTimeFormatter#ISO_LOCAL_DATE_TIME + */ public class TaskHandler extends BaseHttpHandler { public TaskHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) { super(manager, gson, jsonBuilder); } + /** + * Обрабатывает HTTP-метод OPTIONS для задач. + *

+ * Возвращает разрешенные методы в зависимости от контекста: + *

+ *
    + *
  • Для /tasks или /subtasks: GET, POST, DELETE, OPTIONS, HEAD
  • + *
  • Для /tasks/{id} или /subtasks/{id}: GET, DELETE, OPTIONS, HEAD
  • + *
+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + */ @Override protected void handleOptions(HttpExchange exchange, RequestSegments segments) throws IOException { if (segments.id() == 0) { @@ -29,7 +92,20 @@ protected void handleOptions(HttpExchange exchange, RequestSegments segments) th exchange.sendResponseHeaders(204, -1); } - + /** + * Обрабатывает HTTP-метод GET для задач. + *

+ * В зависимости от наличия идентификатора: + *

+ *
    + *
  • Без Id: возвращает список всех задач
  • + *
  • С Id: возвращает конкретную задачу
  • + *
+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + */ @Override protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException { @@ -45,6 +121,29 @@ protected void handleGet(HttpExchange exchange, RequestSegments segments) throws } } + /** + * Обрабатывает HTTP-метод POST для задач. + *

+ * Создает новую задачу или обновляет существующую в зависимости от taskId: + *

+ *
    + *
  • taskId = 0: создание новой задачи
  • + *
  • taskId > 0: обновление существующей задачи
  • + *
+ *

Возвращает задачу в теле ответа

+ * + *

Валидация:

+ *
    + *
  • Проверка
  • + *
  • Проверка наличия обязательных полей
  • + *
  • Проверка формата даты и времени
  • + *
  • Проверка на пересечение временных интервалов
  • + *
+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + */ @Override protected void handlePost(HttpExchange exchange, RequestSegments segments) throws IOException { if (isPostForbidden(exchange, segments)) { @@ -66,19 +165,29 @@ protected void handlePost(HttpExchange exchange, RequestSegments segments) throw manager.updateTask(task); } sendText(exchange, gson.toJson(manager.getTaskWithoutHistory(task.getTaskId())), 201); - } catch (JsonSyntaxException e) { - System.out.println(e); - sendText(exchange, - jsonBuilder.badRequest( - "Json must contain: taskId, title, description and status[NEW, IN_PROGRESS, DONE]."), - 400); } catch (TaskNotFound e) { sendNotFound(exchange, jsonBuilder.notFound(e.getMessage())); + } catch (DateTimeParseException e) { + sendText(exchange, jsonBuilder.badRequest("Invalid date format"), 400); } catch (TaskTimeOverlapException e) { sendHasOverlaps(exchange, jsonBuilder.hasOverlaps()); } } + /** + * Обрабатывает HTTP-метод DELETE для задач. + *

+ * В зависимости от наличия идентификатора: + *

+ *
    + *
  • Без ID: удаляет все задачи
  • + *
  • С ID: удаляет конкретную задачу
  • + *
+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @throws IOException если возникает ошибка при отправке ответа + */ @Override protected void handleDelete(HttpExchange exchange, RequestSegments segments) throws IOException { if (segments.id() == 0) { @@ -90,6 +199,17 @@ protected void handleDelete(HttpExchange exchange, RequestSegments segments) thr } } + /** + * Проверяет корректность ресурса в запросе. + *

+ * Убеждается, что запрос адресован именно задачам ("tasks"). + *

+ * + * @param exchange HTTP-обмен + * @param segments сегменты запроса + * @return true если ресурс неверен (не "tasks"), false если корректно + * @throws IOException если возникает ошибка при отправке ответа 404 + */ @Override protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException { if (!"tasks".equals(segments.resource())) { @@ -99,6 +219,20 @@ protected boolean validateResources(HttpExchange exchange, RequestSegments segme return false; } + /** + * Проверяет валидность объекта задачи. + *

+ * Проверяет наличие обязательных полей: + *

+ *
    + *
  • title - заголовок задачи
  • + *
  • description - описание задачи
  • + *
  • status - статус задачи
  • + *
+ * + * @param task задача для валидации + * @throws JsonSyntaxException если отсутствуют обязательные поля + */ protected void isTaskValid(Task task) { if (task.getTitle() == null || task.getDescription() == null || task.getStatus() == null) { throw new JsonSyntaxException("Task has invalid fields"); diff --git a/src/util/http/RequestSegments.java b/src/util/http/RequestSegments.java index 6d6a7d2..36d56ff 100644 --- a/src/util/http/RequestSegments.java +++ b/src/util/http/RequestSegments.java @@ -7,6 +7,36 @@ import java.util.List; import java.util.Optional; +/** + * Класс для парсинга сегментов HTTP-запроса. + *

+ * Разбирает URI пути запроса на составные части: endpoint, ресурс, + * идентификатор и опциональный подресурс. Также выполняет валидацию HTTP-метода. + *

+ * + *

Формат пути:

+ *
    + *
  • {@code /resource} - ресурс без идентификатора
  • + *
  • {@code /resource/123} - ресурс с числовым идентификатором
  • + *
  • {@code /resource/123/subresource} - ресурс с идентификатором и подресурсом
  • + *
+ * + *

Поддерживаемые HTTP-методы: GET, POST, DELETE, HEAD, OPTIONS

+ * + *

Особенности обработки:

+ *
    + *
  • Метод HEAD обрабатывается как GET
  • + *
  • Некорректные идентификаторы преобразуются в -1
  • + *
  • Неподдерживаемые методы возвращают соответствующие + * значения Endpoint.INVALID_METHOD
  • + *
  • Ошбика парсинга Endpoint возвращает Enpoint.INVALID
  • + *
+ * + * @param endpoint конечная точка запроса (на основе HTTP-метода) + * @param resource название основного ресурса (первый сегмент пути после /) + * @param id числовой идентификатор ресурса (второй сегмент пути) + * @param subResource опциональный подресурс (третий сегмент пути) + */ public record RequestSegments( Endpoint endpoint, String resource, @@ -21,11 +51,39 @@ public record RequestSegments( "OPTIONS") ); + /** + * Создает экземпляр RequestSegments на основе HttpExchange. + *

+ * Извлекает метод запроса и путь из переданного HttpExchange и делегирует + * парсинг методу {@link #parse(String, String)}. + *

+ * + * @param exchange HTTP-обмен для парсинга + * @return новый экземпляр RequestSegments с разобранными сегментами пути + * @see #parse(String, String) + */ public static RequestSegments getRequestSegments(HttpExchange exchange) { String path = exchange.getRequestURI().getPath(); return RequestSegments.parse(exchange.getRequestMethod(), path); } + /** + * Парсит HTTP-метод и путь на составляющие сегменты. + *

+ * В зависимости от количества сегментов пути создает соответствующий экземпляр: + *

+ * + * + * + * + * + * + *
СегментовФорматРезультат
2/resourceresource, id=0, subResource=empty
3/resource/123resource, id=123, subResource=empty
4/resource/123/subresource, id=123, subResource=sub
другоелюбойEndpoint.INVALID_SUBRESOURCE
+ * + * @param method HTTP-метод запроса + * @param path путь URI запроса + * @return экземпляр RequestSegments с разобранными сегментами + */ private static RequestSegments parse(String method, String path) { String[] parts = path.split("/"); @@ -85,6 +143,4 @@ private static Endpoint parseEndpoint(String method) { return endpoint; } - - } From 910ccac0d84664860ad62ed34ab0206eeb7b3a4d Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 24 Sep 2025 03:01:48 +0300 Subject: [PATCH 16/22] refactor: docs fix, updated README --- README.md | 168 ++++++++++++++++++++----- src/http/handlers/EpicsHandler.java | 2 +- src/http/handlers/SubTasksHandler.java | 2 +- 3 files changed, 138 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index be7c42d..9e28842 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,147 @@ -# java-kanban -### Основа для менеджера задач +# Java Kanban - Менеджер задач ---- -## Содержание -- #### Main - пример добавления задачи -- #### TaskManager - основные методы работы программы -- #### Task - родитель всех задач -- #### Epic - задача, способная хранить другие задачи -- #### SubTask - задача, принадлежащая эпику -- #### Status - статусы задач +## Описание проекта ---- +**Java Kanban** - это бэкенд-система для управления задачами, реализующая менеджер задач и HTTP-сервер. -## Особенности задач -### Каждая задача обладает: +Система поддерживает три типа задач: обычные задачи(Task), эпики(Epic) и подзадачи(SubTask). -- #### уникальным id -- #### названием -- #### описанием -- #### статусом +## Функциональные возможности -#### Эпик хранит каждую подзадачу, которая в него входит -#### Подзадача хранит id эпика, которому принадлежит +### Управление задачами +- Создание, обновление и удаление задач всех типов +- Хранение истории просмотров задач +- Очистка всей истории просмотров +- Обработка ошибок при создании и обновлении задач ---- +### Получение информации +- Получение списков всех задач/эпиков/подзадач +- Поиск задач по ID +- Получение всех подзадач конкретного эпика +- Получение задач в порядке приоритета (по времени начала) -## TaskManager -Здесь хранятся основным методы работы с задачами и сами задачи -в таблицах (id=задача) +## Технологический стек +- **Java** - основной язык программирования +- **JUnit 5** - фреймворк для модульного тестирования +- **Gson** - библиотека для работы с JSON -- Добавление задачи в таблицу -- Получение задачи по id -- Удаление задачи из таблицы -- Удаление всех задач или всех задач из определенной задачи -- Печать задач в разных форматах -- Обновление задачи (задача меняет свое название или описание) +## API -Пользователь способен обновить статус задачи, обновив саму задачу. -При это обновление статуса эпика происходит независимо от пользователя, -а благодаря вычислениям +### Поддерживаемые форматы времени +Система поддерживает следующие форматы для полей `startTime`: +- `DateTimeFormatter.ISO_LOCAL_DATE_TIME` - `2023-10-15T14:30:00` +- `DateTimeFormatter.ISO_DATE_TIME` - `2023-10-15T14:30:00.000+03:00` +- `yyyy-MM-dd HH:mm:ss.SSS` - `2023-10-15 14:30:00.000` +- `yyyy-MM-dd HH:mm:ss` - `2023-10-15 14:30:00` +- `dd.MM.yyyy HH:mm:ss` - `15.10.2023 14:30:00` + +### Коды состояния HTTP + +Система использует следующие HTTP коды состояния для обработки запросов: + +| Код | Описание | +|---------|-----------------------------------------------------------------------------------------------------------| +| **200** | Успешный запрос (GET, HEAD, DELETE, OPTIONS) | +| **201** | Успешное создание или обновление ресурса (POST) | +| **400** | Неверный формат JSON или отсутствуют обязательные поля, неверный формат времени, id не число или меньше 0 | +| **404** | Задача не найдена или неверный путь | +| **405** | Метод не разрешен для ресурса | +| **406** | Пересечение временных интервалов с существующими задачами | +| **500** | Внутренняя ошибка сервера | + +### Задачи (Tasks) + +| Метод | Путь | Действие | Коды ответа | +|-------|------|----------|-------------| +| HEAD | `/tasks` или `/tasks/{id}` | Получить заголовки | 200, 404 | +| GET | `/tasks` | Получить все задачи | 200 | +| GET | `/tasks/{id}` | Получить задачу по ID | 200, 404 | +| POST | `/tasks` | Создать новую задачу | 201, 400, 406 | +| DELETE | `/tasks` | Удалить все задачи | 200 | +| DELETE | `/tasks/{id}` | Удалить задачу по ID | 200, 404 | +| OPTIONS | `/tasks` или `/tasks/{id}` | Получить разрешенные методы | 200 | + +#### Формат JSON для задачи +```json +{ + "taskId": 0, // 0 - для создания, >0 - для обновления (обязательное) + "title": "string", // обязательное поле + "description": "string", // обязательное поле + "status": "NEW|IN_PROGRESS|DONE", // обязательное поле + "duration": 10, // продолжительность в минутах (опционально) + "startTime": "1970-01-01T00:00:00.000" // дата и время ISO (опционально) +} +``` + +### Эпики (Epics) + +| Метод | Путь | Действие | Коды ответа | +|-------|------|----------|-------------| +| HEAD | любой путь | Получить заголовки | 200, 404 | +| GET | `/epics` | Получить все эпики | 200 | +| GET | `/epics/{id}` | Получить эпик по ID | 200, 404 | +| GET | `/epics/{id}/subtasks` | Получить все подзадачи эпика | 200, 404 | +| POST | `/epics` | Создать новый эпик | 201, 400, 406 | +| DELETE | `/epics` | Удалить все эпики | 200 | +| DELETE | `/epics/{id}` | Удалить эпик по ID | 200, 404 | +| DELETE | `/epics/{id}/subtasks` | Удалить все подзадачи эпика | 200, 404 | +| OPTIONS | любой путь | Получить разрешенные методы | 200 | + +#### Формат JSON для эпика +```json +{ + "taskId": 0, //0 - для создания, >0 - для обновления (обязательное) + "title": "string", // обязательное поле + "description": "string", // обязательное поле + "status": "NEW|IN_PROGRESS|DONE", // только для чтения + "subtasks": [1, 2, 3], // только для чтения + "duration": 10, // только для чтения + "startTime": "1970-01-01T00:00:00.000" // только для чтения +} +``` + +### Подзадачи (Subtasks) + +| Метод | Путь | Действие | Коды ответа | +|-------|------|----------|-------------| +| HEAD | `/subtasks` или `/subtasks/{id}` | Получить заголовки | 200, 404 | +| GET | `/subtasks` | Получить все подзадачи | 200 | +| GET | `/subtasks/{id}` | Получить подзадачу по ID | 200, 404 | +| POST | `/subtasks` | Создать новую подзадачу | 201, 400, 406 | +| DELETE | `/subtasks` | Удалить все подзадачи | 200 | +| DELETE | `/subtasks/{id}` | Удалить подзадачу по ID | 200, 404 | + +#### Формат JSON для подзадачи +```json +{ + "taskId": 0, // 0 - для создания, >0 - для обновления (обязательное) + "title": "string", // обязательное поле + "description": "string", // обязательное поле + "status": "NEW|IN_PROGRESS|DONE", // обязательное поле + "epicId": 123, // обязательное поле - ID родительского эпика + "duration": 10, // продолжительность в минутах (опционально) + "startTime": "1970-01-01T00:00:00.000" // дата и время ISO (опционально) +} +``` + +## История просмотров (History) + +| Метод | Путь | Действие | Коды ответа | +|-------|------|----------|-------------| +| HEAD | `/history` | Получить заголовки | 200 | +| GET | `/history` | Получить историю просмотров | 200 | +| OPTIONS | `/history` | Получить разрешенные методы | 200 | + +## Список приоритетов (Prioritized) + +| Метод | Путь | Действие | Коды ответа | +|-------|------|----------|-------------| +| HEAD | `/prioritized` | Получить заголовки | 200 | +| GET | `/prioritized` | Получить отсортированный список задач | 200 | +| OPTIONS | `/prioritized` | Получить разрешенные методы | 200 | + +## Тестирование + +Проект включает модульные тесты с использованием JUnit 5 для проверки функциональности менеджера задач и HTTP-сервера. \ No newline at end of file diff --git a/src/http/handlers/EpicsHandler.java b/src/http/handlers/EpicsHandler.java index fecf388..0eead44 100644 --- a/src/http/handlers/EpicsHandler.java +++ b/src/http/handlers/EpicsHandler.java @@ -23,7 +23,7 @@ *

* *

Поддерживаемые эндпоинты для эпиков:

- * + *
* * * diff --git a/src/http/handlers/SubTasksHandler.java b/src/http/handlers/SubTasksHandler.java index 6ed8329..4b43e5d 100644 --- a/src/http/handlers/SubTasksHandler.java +++ b/src/http/handlers/SubTasksHandler.java @@ -23,7 +23,7 @@ *

* *

Поддерживаемые эндпоинты для подзадач:

- *
МетодПутьДействие
HEADлюбой путьПолучить заголовки
GET/epicsПолучить все эпики
+ *
* * * From 141627cdd7e213f3cb237ea4d1487df744542d6f Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 24 Sep 2025 03:04:17 +0300 Subject: [PATCH 17/22] refactor: spell fix --- src/util/http/RequestSegments.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/util/http/RequestSegments.java b/src/util/http/RequestSegments.java index 36d56ff..f27a6a5 100644 --- a/src/util/http/RequestSegments.java +++ b/src/util/http/RequestSegments.java @@ -32,9 +32,9 @@ *
  • Ошбика парсинга Endpoint возвращает Enpoint.INVALID
  • * * - * @param endpoint конечная точка запроса (на основе HTTP-метода) - * @param resource название основного ресурса (первый сегмент пути после /) - * @param id числовой идентификатор ресурса (второй сегмент пути) + * @param endpoint конечная точка запроса (на основе HTTP-метода) + * @param resource название основного ресурса (первый сегмент пути после /) + * @param id числовой идентификатор ресурса (второй сегмент пути) * @param subResource опциональный подресурс (третий сегмент пути) */ public record RequestSegments( @@ -81,7 +81,7 @@ public static RequestSegments getRequestSegments(HttpExchange exchange) { *
    МетодПутьДействие
    HEAD/subtasks или /subtasks/{id}Получить заголовки
    GET/subtasksПолучить все подзадачи
    * * @param method HTTP-метод запроса - * @param path путь URI запроса + * @param path путь URI запроса * @return экземпляр RequestSegments с разобранными сегментами */ private static RequestSegments parse(String method, String path) { From fd1e7391f3b7b79429b8a08adfa0692cd30e8f70 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 24 Sep 2025 03:08:39 +0300 Subject: [PATCH 18/22] fix: remove static key-word --- src/util/gsonadapters/LocalDateTimeAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/gsonadapters/LocalDateTimeAdapter.java b/src/util/gsonadapters/LocalDateTimeAdapter.java index 40d269b..532077a 100644 --- a/src/util/gsonadapters/LocalDateTimeAdapter.java +++ b/src/util/gsonadapters/LocalDateTimeAdapter.java @@ -11,7 +11,7 @@ import java.util.List; public class LocalDateTimeAdapter extends TypeAdapter { - private static final List formats = + private final List formats = List.of(DateTimeFormatter.ISO_LOCAL_DATE_TIME, DateTimeFormatter.ISO_DATE_TIME, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"), From 077bfca3b9ab4482346ecbefaa4835e98847fa86 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 24 Sep 2025 03:30:05 +0300 Subject: [PATCH 19/22] codeStyle: rename DEFAULT_CHARSET -> defaultCharset --- src/http/handlers/BaseHttpHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/http/handlers/BaseHttpHandler.java b/src/http/handlers/BaseHttpHandler.java index c474243..37a1f5e 100644 --- a/src/http/handlers/BaseHttpHandler.java +++ b/src/http/handlers/BaseHttpHandler.java @@ -50,7 +50,7 @@ * для конкретной логики обработки ресурсов.

    */ public abstract class BaseHttpHandler implements HttpHandler { - private final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + private final Charset defaultCharset = StandardCharsets.UTF_8; protected final TaskManager manager; protected Gson gson; @@ -169,7 +169,7 @@ private boolean isSegmentsValid(HttpExchange exchange, RequestSegments segments) protected void sendText(HttpExchange exchange, String responseString, int responseCode) throws IOException { - byte[] resp = responseString.getBytes(DEFAULT_CHARSET); + byte[] resp = responseString.getBytes(defaultCharset); exchange.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); exchange.getResponseHeaders().add("Content-Length", String.valueOf(resp.length)); From 54223512c8f3729a7af35d59718dd8c972856d8d Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 24 Sep 2025 03:31:16 +0300 Subject: [PATCH 20/22] codeStyle: rename PORT -> port --- src/http/HttpTaskServer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/http/HttpTaskServer.java b/src/http/HttpTaskServer.java index c997b3a..0939d82 100644 --- a/src/http/HttpTaskServer.java +++ b/src/http/HttpTaskServer.java @@ -16,7 +16,7 @@ import java.time.LocalDateTime; public class HttpTaskServer { - private final int PORT = 8080; + private final int port = 8080; private HttpServer httpServer; private final TaskManager manager; @@ -39,7 +39,7 @@ public HttpTaskServer(TaskManager manager) { private void createServer() { try { - httpServer = HttpServer.create(new InetSocketAddress(PORT), 0); + httpServer = HttpServer.create(new InetSocketAddress(port), 0); httpServer.createContext("/tasks", new TaskHandler(manager, gson, jsonBuilder)); httpServer.createContext("/epics", new EpicsHandler(manager, gson, jsonBuilder)); httpServer.createContext("/subtasks", new SubTasksHandler(manager, gson, jsonBuilder)); From 091c0cf182ab39cb63322a7164af1d1e0e1165a6 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 24 Sep 2025 03:33:27 +0300 Subject: [PATCH 21/22] codeStyle: made HashSet validMethods local var --- src/util/http/RequestSegments.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/util/http/RequestSegments.java b/src/util/http/RequestSegments.java index f27a6a5..4bb51ab 100644 --- a/src/util/http/RequestSegments.java +++ b/src/util/http/RequestSegments.java @@ -43,14 +43,6 @@ public record RequestSegments( int id, Optional subResource) { - private final static HashSet validMethods = new HashSet<>(List.of( - "GET", - "POST", - "DELETE", - "HEAD", - "OPTIONS") - ); - /** * Создает экземпляр RequestSegments на основе HttpExchange. *

    @@ -125,6 +117,13 @@ private static int parseInt(String idString) { } private static Endpoint parseEndpoint(String method) { + HashSet validMethods = new HashSet<>(List.of( + "GET", + "POST", + "DELETE", + "HEAD", + "OPTIONS") + ); if (!validMethods.contains(method)) { return Endpoint.INVALID_METHOD; } From 00ef440dae34ecadb239af9222cb4790a6db3518 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 24 Sep 2025 03:35:32 +0300 Subject: [PATCH 22/22] codeStyle: ErrorToJson -> errorToJson --- src/util/http/ErrorResponse.java | 2 +- src/util/http/JsonBuilder.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/util/http/ErrorResponse.java b/src/util/http/ErrorResponse.java index 72d914f..3f0196a 100644 --- a/src/util/http/ErrorResponse.java +++ b/src/util/http/ErrorResponse.java @@ -11,7 +11,7 @@ private ErrorResponse(String error, String message) { this.message = message; } - public static String ErrorToJson(Gson gson, String error, String message) { + public static String errorToJson(Gson gson, String error, String message) { return gson.toJson(new ErrorResponse(error, message)); } } \ No newline at end of file diff --git a/src/util/http/JsonBuilder.java b/src/util/http/JsonBuilder.java index ae60c08..32c730c 100644 --- a/src/util/http/JsonBuilder.java +++ b/src/util/http/JsonBuilder.java @@ -28,7 +28,7 @@ public String tooMuchSubResources() { } public String badRequest(String message) { - return ErrorResponse.ErrorToJson( + return ErrorResponse.errorToJson( gson, "Bad Request", message @@ -36,7 +36,7 @@ public String badRequest(String message) { } public String notFound(String message) { - return ErrorResponse.ErrorToJson( + return ErrorResponse.errorToJson( gson, "Not Found", message @@ -52,13 +52,13 @@ public String subresourceNotFound() { } public String hasOverlaps() { - return ErrorResponse.ErrorToJson(gson, "Task time is overlapping", + return ErrorResponse.errorToJson(gson, "Task time is overlapping", "This task cannot be added due to overlap" ); } public String invalidId() { - return ErrorResponse.ErrorToJson( + return ErrorResponse.errorToJson( gson, "Bad Request", "Id must be a positive integer"