-
Notifications
You must be signed in to change notification settings - Fork 0
Sprint 9 solution #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
06767c9
feat: add duration and start time to Task
ksupyl 7853dc7
feat: add duration and start time to Subtask
ksupyl d1eda1c
feat: add calculated time fields to Epic
ksupyl a4aaf79
fix: use defensive copy in Epic setSubtaskIds
ksupyl 6a40426
merge: bring file manager fixes into time-and-duration branch
ksupyl c7c5c46
fix: return full copies of tasks with time fields
ksupyl 7e81a47
feat: add prioritized tasks storage with TreeSet
ksupyl 3b4b597
feat: add task time overlap validation
ksupyl 07ab5ea
feat: calculate epic duration and time based on subtasks
ksupyl 7ae11d2
feat: save and load task time fields in csv
ksupyl 5374a8e
refactor: replace loops with stream api in task processing
ksupyl 9b730bd
fix: save time fields in history snapshots
ksupyl 033fdef
test: add base task manager test class
ksupyl d41c8c4
test: simplify in-memory task manager test setup
ksupyl 3c9ecb5
test: add file-backed task manager persistence tests
ksupyl e430a7a
test: add history manager edge case tests
ksupyl 7cc4bc5
fix: preserve calculated fields in Epic during update
ksupyl 727eee0
fix: prevent creating subtask without existing epic
ksupyl 9f37d13
fix: validate epic existence in updateSubtask()
ksupyl d03d017
refactor: preserve task and subtask status on creation
ksupyl 3c5bf44
refactor: improve safety in getEpicSubtasks()
ksupyl f0c8de0
test: add tests for clear operations in TaskManager
ksupyl ea4d861
test: add tests for update operations in TaskManager
ksupyl 1f552a0
test: add tests for delete operations
ksupyl 7b13134
test: add edge case tests for entity lookup in TaskManager
ksupyl 74db527
test: add history and subtask validation tests in TaskManager
ksupyl fac189f
test: add overlap validation tests for tasks and subtasks
ksupyl c059c85
fix: preserve epic connection when updating subtask
ksupyl 1e5ee6f
test: add tests for epic subtasks and prioritized tasks
ksupyl a4bef12
feat: add base http handler and not found exception
ksupyl f1c4a08
feat: add HttpTaskServer skeleton with endpoint handlers
ksupyl 4c91767
chore: add gson library jar to project
ksupyl 1eb78bb
refactor: move null time validation to hasTimeOverlap
ksupyl 819102f
feat: add Gson adapters for Duration and LocalDateTime
ksupyl fdaf4d6
refactor: throw NotFoundException in InMemoryTaskManager
ksupyl 255c7e7
feat: implement TasksHandler for HTTP API
ksupyl ed7ba7f
feat: implement SubtasksHandler for HTTP API
ksupyl 230dfb5
feat: implement EpicsHandler for HTTP API
ksupyl 6e2adad
feat: implement HistoryHandler for HTTP API
ksupyl ae6e088
feat: implement PrioritizedHandler for HTTP API
ksupyl 5c3e453
test: add HTTP tests for tasks endpoint
ksupyl e248d64
test: update TaskManager tests for NotFoundException behavior
ksupyl c124b81
test: add HTTP tests for epics and subtasks endpoints
ksupyl 11548aa
test: add HTTP tests for history and prioritized endpoints
ksupyl 8517c99
test: add missing HTTP API tests for update and not found cases
ksupyl ff98704
refactor: extract HTTP status codes to constants in handlers
ksupyl c414e86
refactor: extract shared constants and URI builder for HTTP tests
ksupyl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Binary file not shown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package http; | ||
|
|
||
| import com.google.gson.Gson; | ||
| import com.google.gson.GsonBuilder; | ||
| import com.sun.net.httpserver.HttpServer; | ||
| import http.adapter.DurationAdapter; | ||
| import http.adapter.LocalDateTimeAdapter; | ||
| import http.handler.EpicsHandler; | ||
| import http.handler.HistoryHandler; | ||
| import http.handler.PrioritizedHandler; | ||
| import http.handler.SubtasksHandler; | ||
| import http.handler.TasksHandler; | ||
| import service.Managers; | ||
| import service.TaskManager; | ||
|
|
||
| import java.io.IOException; | ||
| import java.net.InetSocketAddress; | ||
| import java.time.Duration; | ||
| import java.time.LocalDateTime; | ||
|
|
||
| public class HttpTaskServer { | ||
|
|
||
| // Порт, на котором будет работать сервер | ||
| private static final int PORT = 8080; | ||
|
|
||
| // Экземпляр HTTP-сервера из стандартной библиотеки Java | ||
| private final HttpServer httpServer; | ||
|
|
||
| // Менеджер задач, с которым будут работать обработчики запросов | ||
| private final TaskManager taskManager; | ||
|
|
||
| // Gson нужен для преобразования объектов Java в JSON и обратно | ||
| private static final Gson GSON = new GsonBuilder() | ||
| .registerTypeAdapter(Duration.class, new DurationAdapter()) | ||
| .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter()) | ||
| .create(); | ||
|
|
||
| // Конструктор для обычного запуска приложения и для тестов | ||
| public HttpTaskServer(TaskManager taskManager) throws IOException { | ||
| this.taskManager = taskManager; | ||
| this.httpServer = HttpServer.create(new InetSocketAddress(PORT), 0); | ||
|
|
||
| // Регистрируем обработчики для каждого базового пути API | ||
| httpServer.createContext("/tasks", new TasksHandler(this.taskManager)); | ||
| httpServer.createContext("/subtasks", new SubtasksHandler(this.taskManager)); | ||
| httpServer.createContext("/epics", new EpicsHandler(this.taskManager)); | ||
| httpServer.createContext("/history", new HistoryHandler(this.taskManager)); | ||
| httpServer.createContext("/prioritized", new PrioritizedHandler(this.taskManager)); | ||
| } | ||
|
|
||
| // Метод запуска сервера | ||
| public void start() { | ||
| httpServer.start(); | ||
| System.out.println("HTTP task server started on port " + PORT); | ||
| } | ||
|
|
||
| // Метод остановки сервера | ||
| public void stop() { | ||
| httpServer.stop(0); | ||
| System.out.println("HTTP task server stopped on port " + PORT); | ||
| } | ||
|
|
||
| // Общий Gson для всего HTTP-слоя | ||
| public static Gson getGson() { | ||
| return GSON; | ||
| } | ||
|
|
||
| // Точка входа в приложение | ||
| public static void main(String[] args) throws IOException { | ||
| TaskManager manager = Managers.getDefault(); | ||
| HttpTaskServer httpTaskServer = new HttpTaskServer(manager); | ||
| httpTaskServer.start(); | ||
| } | ||
|
|
||
|
|
||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package http.adapter; | ||
|
|
||
| import com.google.gson.JsonDeserializationContext; | ||
| import com.google.gson.JsonDeserializer; | ||
| import com.google.gson.JsonElement; | ||
| import com.google.gson.JsonParseException; | ||
| import com.google.gson.JsonPrimitive; | ||
| import com.google.gson.JsonSerializationContext; | ||
| import com.google.gson.JsonSerializer; | ||
|
|
||
| import java.lang.reflect.Type; | ||
| import java.time.Duration; | ||
|
|
||
| // Адаптер для преобразования Duration в JSON и обратно. | ||
| // В JSON будет храниться продолжительность как количество минут. | ||
| public class DurationAdapter implements JsonSerializer<Duration>, JsonDeserializer<Duration> { | ||
|
|
||
| // Преобразование объекта Duration в JSON | ||
| @Override | ||
| public JsonElement serialize(Duration duration, Type typeOfSrc, JsonSerializationContext context) { | ||
| if (duration == null) { | ||
| return null; | ||
| } | ||
|
|
||
| return new JsonPrimitive(duration.toMinutes()); | ||
| } | ||
|
|
||
| // Преобразование JSON обратно в объект Duration | ||
| @Override | ||
| public Duration deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) | ||
| throws JsonParseException { | ||
| if (json == null || json.isJsonNull()) { | ||
| return null; | ||
| } | ||
|
|
||
| return Duration.ofMinutes(json.getAsLong()); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package http.adapter; | ||
|
|
||
| import com.google.gson.JsonDeserializationContext; | ||
| import com.google.gson.JsonDeserializer; | ||
| import com.google.gson.JsonElement; | ||
| import com.google.gson.JsonParseException; | ||
| import com.google.gson.JsonPrimitive; | ||
| import com.google.gson.JsonSerializationContext; | ||
| import com.google.gson.JsonSerializer; | ||
|
|
||
| import java.lang.reflect.Type; | ||
| import java.time.LocalDateTime; | ||
|
|
||
| // Адаптер для преобразования LocalDateTime в JSON и обратно. | ||
| // В JSON дата и время будут храниться в строковом ISO-формате. | ||
| public class LocalDateTimeAdapter implements JsonSerializer<LocalDateTime>, JsonDeserializer<LocalDateTime> { | ||
|
|
||
| // Преобразование LocalDateTime в JSON | ||
| @Override | ||
| public JsonElement serialize(LocalDateTime localDateTime, Type typeOfSrc, JsonSerializationContext context) { | ||
| if (localDateTime == null) { | ||
| return null; | ||
| } | ||
|
|
||
| return new JsonPrimitive(localDateTime.toString()); | ||
| } | ||
|
|
||
| // Преобразование JSON обратно в LocalDateTime | ||
| @Override | ||
| public LocalDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) | ||
| throws JsonParseException { | ||
| if (json == null || json.isJsonNull()) { | ||
| return null; | ||
| } | ||
|
|
||
| return LocalDateTime.parse(json.getAsString()); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package http.handler; | ||
|
|
||
| import com.sun.net.httpserver.HttpExchange; | ||
|
|
||
| import java.io.IOException; | ||
| import java.nio.charset.StandardCharsets; | ||
|
|
||
| // Базовый класс для всех HTTP-обработчиков. | ||
| // Содержит общие методы для отправки и чтения HTTP-данных. | ||
| public class BaseHttpHandler { | ||
|
|
||
| // HTTP-статусы, используемые в обработчиках. | ||
| protected static final int STATUS_OK = 200; | ||
| protected static final int STATUS_CREATED = 201; | ||
| protected static final int STATUS_NOT_FOUND = 404; | ||
| protected static final int STATUS_NOT_ACCEPTABLE = 406; | ||
| protected static final int STATUS_INTERNAL_ERROR = 500; | ||
|
|
||
| // Отправка ответа с текстом в формате JSON. | ||
| protected void sendText(HttpExchange exchange, String text, int statusCode) throws IOException { | ||
| byte[] response = text.getBytes(StandardCharsets.UTF_8); | ||
|
|
||
| exchange.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); | ||
| exchange.sendResponseHeaders(statusCode, response.length); | ||
| exchange.getResponseBody().write(response); | ||
| exchange.close(); | ||
| } | ||
|
|
||
| // Отправка ответа 201 без тела. | ||
| protected void sendCreated(HttpExchange exchange) throws IOException { | ||
| exchange.sendResponseHeaders(STATUS_CREATED, -1); | ||
| exchange.close(); | ||
| } | ||
|
|
||
| // Чтение тела HTTP-запроса в виде строки. | ||
| protected String readText(HttpExchange exchange) throws IOException { | ||
| return new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); | ||
| } | ||
|
|
||
| // Ответ 404 — объект не найден. | ||
| protected void sendNotFound(HttpExchange exchange) throws IOException { | ||
| exchange.sendResponseHeaders(STATUS_NOT_FOUND, -1); | ||
| exchange.close(); | ||
| } | ||
|
|
||
| // Ответ 406 — задача пересекается по времени. | ||
| protected void sendHasInteractions(HttpExchange exchange) throws IOException { | ||
| exchange.sendResponseHeaders(STATUS_NOT_ACCEPTABLE, -1); | ||
| exchange.close(); | ||
| } | ||
|
|
||
| // Ответ 500 — внутренняя ошибка сервера. | ||
| protected void sendInternalError(HttpExchange exchange) throws IOException { | ||
| exchange.sendResponseHeaders(STATUS_INTERNAL_ERROR, -1); | ||
| exchange.close(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| package http.handler; | ||
|
|
||
| import com.google.gson.Gson; | ||
| import com.sun.net.httpserver.HttpExchange; | ||
| import com.sun.net.httpserver.HttpHandler; | ||
| import http.HttpTaskServer; | ||
| import model.Epic; | ||
| import model.Subtask; | ||
| import service.TaskManager; | ||
| import service.exception.NotFoundException; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.List; | ||
|
|
||
| // Обработчик запросов по пути /epics. | ||
| // Поддерживает получение списка эпиков, получение эпика по id, | ||
| // получение подзадач эпика, создание/обновление эпика и удаление эпика. | ||
| public class EpicsHandler extends BaseHttpHandler implements HttpHandler { | ||
|
|
||
| // Менеджер задач, с которым работает обработчик. | ||
| private final TaskManager taskManager; | ||
|
|
||
| // Общий Gson для преобразования объектов в JSON и обратно. | ||
| private final Gson gson = HttpTaskServer.getGson(); | ||
|
|
||
| public EpicsHandler(TaskManager taskManager) { | ||
| this.taskManager = taskManager; | ||
| } | ||
|
|
||
| // Главный метод обработки HTTP-запроса. | ||
| @Override | ||
| public void handle(HttpExchange exchange) throws IOException { | ||
| try { | ||
| String method = exchange.getRequestMethod(); | ||
| String path = exchange.getRequestURI().getPath(); | ||
|
|
||
| if ("GET".equals(method)) { | ||
| handleGet(exchange, path); | ||
| return; | ||
| } | ||
|
|
||
| if ("POST".equals(method)) { | ||
| handlePost(exchange, path); | ||
| return; | ||
| } | ||
|
|
||
| if ("DELETE".equals(method)) { | ||
| handleDelete(exchange, path); | ||
| return; | ||
| } | ||
|
|
||
| sendInternalError(exchange); | ||
|
|
||
| } catch (NotFoundException e) { | ||
| sendNotFound(exchange); | ||
| } catch (IllegalArgumentException e) { | ||
| sendHasInteractions(exchange); | ||
| } catch (Exception e) { | ||
| sendInternalError(exchange); | ||
| } | ||
| } | ||
|
|
||
| // Обработка GET-запросов: | ||
| // GET /epics -> список всех эпиков | ||
| // GET /epics/{id} -> один эпик по id | ||
| // GET /epics/{id}/subtasks -> список подзадач эпика | ||
| private void handleGet(HttpExchange exchange, String path) throws IOException { | ||
| if ("/epics".equals(path)) { | ||
| List<Epic> epics = taskManager.getEpics(); | ||
| String response = gson.toJson(epics); | ||
| sendText(exchange, response, STATUS_OK); | ||
| return; | ||
| } | ||
|
|
||
| if (path.endsWith("/subtasks")) { | ||
| int epicId = extractEpicIdForSubtasks(path); | ||
| List<Subtask> subtasks = taskManager.getEpicSubtasks(epicId); | ||
| String response = gson.toJson(subtasks); | ||
| sendText(exchange, response, STATUS_OK); | ||
| return; | ||
| } | ||
|
|
||
| int id = extractId(path); | ||
| Epic epic = taskManager.getEpic(id); | ||
| String response = gson.toJson(epic); | ||
| sendText(exchange, response, STATUS_OK); | ||
| } | ||
|
|
||
| // Обработка POST-запроса: | ||
| // POST /epics -> создание нового эпика или обновление существующего. | ||
| private void handlePost(HttpExchange exchange, String path) throws IOException { | ||
| if (!"/epics".equals(path)) { | ||
| throw new NotFoundException("Некорректный путь для POST /epics"); | ||
| } | ||
|
|
||
| String body = readText(exchange); | ||
| Epic epic = gson.fromJson(body, Epic.class); | ||
|
|
||
| if (epic == null) { | ||
| throw new IOException("Тело запроса пустое."); | ||
| } | ||
|
|
||
| if (epic.getId() == 0) { | ||
| taskManager.createEpic(epic); | ||
| } else { | ||
| taskManager.updateEpic(epic); | ||
| } | ||
|
|
||
| sendCreated(exchange); | ||
| } | ||
|
|
||
| // Обработка DELETE-запроса: | ||
| // DELETE /epics/{id} -> удаление эпика по id. | ||
| private void handleDelete(HttpExchange exchange, String path) throws IOException { | ||
| int id = extractId(path); | ||
| taskManager.deleteEpic(id); | ||
| sendText(exchange, "", STATUS_OK); | ||
| } | ||
|
|
||
| // Извлечение id эпика из пути вида /epics/{id}. | ||
| private int extractId(String path) { | ||
| String[] pathParts = path.split("/"); | ||
|
|
||
| if (pathParts.length != 3) { | ||
| throw new NotFoundException("Некорректный путь запроса."); | ||
| } | ||
|
|
||
| try { | ||
| return Integer.parseInt(pathParts[2]); | ||
| } catch (NumberFormatException e) { | ||
| throw new NotFoundException("Некорректный идентификатор эпика."); | ||
| } | ||
| } | ||
|
|
||
| // Извлечение id эпика из пути вида /epics/{id}/subtasks. | ||
| private int extractEpicIdForSubtasks(String path) { | ||
| String[] pathParts = path.split("/"); | ||
|
|
||
| if (pathParts.length != 4) { | ||
| throw new NotFoundException("Некорректный путь запроса."); | ||
| } | ||
|
|
||
| if (!"subtasks".equals(pathParts[3])) { | ||
| throw new NotFoundException("Некорректный путь запроса."); | ||
| } | ||
|
|
||
| try { | ||
| return Integer.parseInt(pathParts[2]); | ||
| } catch (NumberFormatException e) { | ||
| throw new NotFoundException("Некорректный идентификатор эпика."); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.