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-методам
+ * - Стандартизированную отправку ответов
+ * - Обработку ошибок и исключений
+ *
+ *
+ * Жизненный цикл обработки запроса:
+ *
+ * - Парсинг сегментов пути из URI
+ * - Валидация сегментов и HTTP-метода
+ * - Проверка ресурсов (абстрактный метод)
+ * - Маршрутизация на соответствующий обработчик метода
+ * - Обработка исключений и отправка ошибок
+ *
+ *
+ * Поддерживаемые 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 | /resource | resource, id=0, subResource=empty |
+ * | 3 | /resource/123 | resource, id=123, subResource=empty |
+ * | 4 | /resource/123/sub | resource, 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 @@
*
*
* Поддерживаемые эндпоинты для эпиков:
- *
+ *
* | Метод | Путь | Действие |
* | HEAD | любой путь | Получить заголовки |
* | GET | /epics | Получить все эпики |
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 | /subtasks или /subtasks/{id} | Получить заголовки |
* | GET | /subtasks | Получить все подзадачи |
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) {
*
*
* @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"