Skip to content
Merged
Show file tree
Hide file tree
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 Apr 14, 2026
7853dc7
feat: add duration and start time to Subtask
ksupyl Apr 14, 2026
d1eda1c
feat: add calculated time fields to Epic
ksupyl Apr 14, 2026
a4aaf79
fix: use defensive copy in Epic setSubtaskIds
ksupyl Apr 14, 2026
6a40426
merge: bring file manager fixes into time-and-duration branch
ksupyl Apr 15, 2026
c7c5c46
fix: return full copies of tasks with time fields
ksupyl Apr 16, 2026
7e81a47
feat: add prioritized tasks storage with TreeSet
ksupyl Apr 16, 2026
3b4b597
feat: add task time overlap validation
ksupyl Apr 16, 2026
07ab5ea
feat: calculate epic duration and time based on subtasks
ksupyl Apr 16, 2026
7ae11d2
feat: save and load task time fields in csv
ksupyl Apr 16, 2026
5374a8e
refactor: replace loops with stream api in task processing
ksupyl Apr 16, 2026
9b730bd
fix: save time fields in history snapshots
ksupyl Apr 16, 2026
033fdef
test: add base task manager test class
ksupyl Apr 16, 2026
d41c8c4
test: simplify in-memory task manager test setup
ksupyl Apr 16, 2026
3c9ecb5
test: add file-backed task manager persistence tests
ksupyl Apr 16, 2026
e430a7a
test: add history manager edge case tests
ksupyl Apr 16, 2026
7cc4bc5
fix: preserve calculated fields in Epic during update
ksupyl Apr 16, 2026
727eee0
fix: prevent creating subtask without existing epic
ksupyl Apr 16, 2026
9f37d13
fix: validate epic existence in updateSubtask()
ksupyl Apr 16, 2026
d03d017
refactor: preserve task and subtask status on creation
ksupyl Apr 16, 2026
3c5bf44
refactor: improve safety in getEpicSubtasks()
ksupyl Apr 16, 2026
f0c8de0
test: add tests for clear operations in TaskManager
ksupyl Apr 16, 2026
ea4d861
test: add tests for update operations in TaskManager
ksupyl Apr 16, 2026
1f552a0
test: add tests for delete operations
ksupyl Apr 16, 2026
7b13134
test: add edge case tests for entity lookup in TaskManager
ksupyl Apr 16, 2026
74db527
test: add history and subtask validation tests in TaskManager
ksupyl Apr 16, 2026
fac189f
test: add overlap validation tests for tasks and subtasks
ksupyl Apr 16, 2026
c059c85
fix: preserve epic connection when updating subtask
ksupyl Apr 16, 2026
1e5ee6f
test: add tests for epic subtasks and prioritized tasks
ksupyl Apr 16, 2026
a4bef12
feat: add base http handler and not found exception
ksupyl Apr 17, 2026
f1c4a08
feat: add HttpTaskServer skeleton with endpoint handlers
ksupyl Apr 17, 2026
4c91767
chore: add gson library jar to project
ksupyl Apr 17, 2026
1eb78bb
refactor: move null time validation to hasTimeOverlap
ksupyl Apr 17, 2026
819102f
feat: add Gson adapters for Duration and LocalDateTime
ksupyl Apr 18, 2026
fdaf4d6
refactor: throw NotFoundException in InMemoryTaskManager
ksupyl Apr 18, 2026
255c7e7
feat: implement TasksHandler for HTTP API
ksupyl Apr 18, 2026
ed7ba7f
feat: implement SubtasksHandler for HTTP API
ksupyl Apr 18, 2026
230dfb5
feat: implement EpicsHandler for HTTP API
ksupyl Apr 18, 2026
6e2adad
feat: implement HistoryHandler for HTTP API
ksupyl Apr 18, 2026
ae6e088
feat: implement PrioritizedHandler for HTTP API
ksupyl Apr 18, 2026
5c3e453
test: add HTTP tests for tasks endpoint
ksupyl Apr 18, 2026
e248d64
test: update TaskManager tests for NotFoundException behavior
ksupyl Apr 18, 2026
c124b81
test: add HTTP tests for epics and subtasks endpoints
ksupyl Apr 18, 2026
11548aa
test: add HTTP tests for history and prioritized endpoints
ksupyl Apr 18, 2026
8517c99
test: add missing HTTP API tests for update and not found cases
ksupyl Apr 18, 2026
ff98704
refactor: extract HTTP status codes to constants in handlers
ksupyl Apr 19, 2026
c414e86
refactor: extract shared constants and URI builder for HTTP tests
ksupyl Apr 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added lib/gson-2.9.0.jar
Binary file not shown.
76 changes: 76 additions & 0 deletions src/http/HttpTaskServer.java
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();
}


}
38 changes: 38 additions & 0 deletions src/http/adapter/DurationAdapter.java
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());
}
}
38 changes: 38 additions & 0 deletions src/http/adapter/LocalDateTimeAdapter.java
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());
}
}
57 changes: 57 additions & 0 deletions src/http/handler/BaseHttpHandler.java
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();
}
}
153 changes: 153 additions & 0 deletions src/http/handler/EpicsHandler.java
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 {
Comment thread
ksupyl marked this conversation as resolved.
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("Некорректный идентификатор эпика.");
}
}
}
Loading
Loading