diff --git a/README.md b/README.md index 4a80115..19aec56 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/TvkQWWs6) # Features of modern Java # Цели и задачи л/р: diff --git a/build.gradle.kts b/build.gradle.kts index 79bf52a..49682d3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,6 +9,16 @@ repositories { mavenCentral() } +java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } +} + +tasks.withType().configureEach { + options.compilerArgs.add("--enable-preview") +} + dependencies { testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/src/main/java/org/lab/Main.java b/src/main/java/org/lab/Main.java index 22028ef..a117e82 100644 --- a/src/main/java/org/lab/Main.java +++ b/src/main/java/org/lab/Main.java @@ -1,4 +1,146 @@ -void main() { - IO.println("Hello and welcome!"); +import org.lab.model.Project; +import org.lab.model.Ticket; +import org.lab.model.storage.Storage; +import org.lab.model.user.Developer; +import org.lab.model.user.Manager; +import org.lab.model.user.TeamLead; +import org.lab.model.user.Tester; +import org.lab.model.user.User; + +void main() throws Exception { + var storage = new Storage(); + + var manager = new Manager(UUID.randomUUID()); + var teamLead = new TeamLead(UUID.randomUUID()); + var developer = new Developer(UUID.randomUUID()); + var tester = new Tester(UUID.randomUUID()); + + storage.addUser(manager); + storage.addUser(teamLead); + storage.addUser(developer); + storage.addUser(tester); + + IO.println("Пользователи созданы"); + + checkWhoCanCreateTickets(List.of(manager, teamLead, developer, tester), storage); + + var project = new Project("Project Alpha", null, List.of(), List.of(), manager, null, List.of()); + storage.addProject(project); + + IO.println("Проект создан: " + project.name()); + + project = storage.setTeamLeadToProject(manager, project, teamLead); + + IO.println("Тимлидер назначен: " + project.teamLead().id()); + + project = storage.addNewDeveloper(manager, project, developer); + project = storage.addNewTester(manager, project, tester); + + IO.println("Разработчик и тестировщик добавлены"); + + var startDate = LocalDateTime.now(); + var endDate = startDate.plusWeeks(2); + + var milestone = storage.createMilestone("Milestone 1", "First development iteration", startDate, endDate); + + project = storage.assignMilestoneToProject(manager, project, milestone); + + IO.println("Майлстоун создан и назначен"); + + var ticket = storage.createTicket("Fix login bug", project, milestone); + var ticket2 = storage.createTicket("Fix SLA bug", project, milestone); + var ticket3 = storage.createTicket("Fix PostgreSQL bug", project, milestone); + + milestone = storage.addTicketToMilestone(ticket, milestone); + milestone = storage.addTicketToMilestone(ticket2, milestone); + milestone = storage.addTicketToMilestone(ticket3, milestone); + + project = project.withNewMilestone(milestone); + + IO.println("Тикет создан: " + ticket.name()); + + ticket = storage.assignDeveloperToTicket(manager, ticket, developer); + + IO.println("Разработчик назначен на тикет"); + + ticket = storage.startWorkingOnTicket(developer, ticket); // IN_PROGRESS + + IO.println("Разработчик завершил тикет"); + + checkAllTicketsStatusAsync(project, storage); + + ticket = storage.completeTicket(teamLead, ticket); + + IO.println("Тикет подтвержден тимлидером"); + + var bug = storage.reportBug("Login crash", "Login crashes after 2nd attempt", project); + + IO.println("Баг найден тестировщиком: " + bug.name()); + + bug = storage.fixBug(developer, bug); + + IO.println("Баг исправлен разработчиком"); + + bug = storage.testBugFix(tester, bug); + + IO.println("Баг проверен тестировщиком"); + + try { + milestone = milestone.close(); + IO.println("Майлстоун успешно закрыт"); + } catch (RuntimeException e) { + System.err.println("Не удалось закрыть майлстоун: " + e.getMessage()); + } + + System.out.printf(""" + Финальный статус проекта: + Проект: %s + Майлстоун: %s (%s) + Тикеты: %d + Баги: %d + Статус бага: %s + %n""", + project.name(), + milestone.name(), + milestone.status(), + project.milestone().tickets().size(), + project.bugReports().size(), + bug.status() + ); +} + +private static void checkAllTicketsStatusAsync(Project project, Storage storage) throws Exception { + try (var scope = StructuredTaskScope.open()) { + for (var ticket : project.milestone().tickets()) { + scope.fork(() -> { + IO.println("Проверяю тикет: " + ticket.name() + " в потоке: " + Thread.currentThread()); + if (ticket.status() == Ticket.Status.IN_PROGRESS) { + storage.startWorkingOnTicket(ticket.developers().getFirst(), ticket); + } + return null; + }); + } + scope.join(); + + IO.println("Все тикеты проверены"); + } } +private static void checkWhoCanCreateTickets(List users, Storage storage) { + try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + var futures = new ArrayList>(); + + for (var user : users) { + futures.add(executor.submit(() -> { + var canCreateTickets = storage.canCreateTicket(user); + IO.println("Пользователь " + user + " может создавать тикеты: " + canCreateTickets); + })); + } + + for (var future : futures) { + future.get(); + } + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } +} diff --git a/src/main/java/org/lab/model/BugReport.java b/src/main/java/org/lab/model/BugReport.java new file mode 100644 index 0000000..d1e66a6 --- /dev/null +++ b/src/main/java/org/lab/model/BugReport.java @@ -0,0 +1,15 @@ +package org.lab.model; + +import java.util.UUID; + +public record BugReport( + UUID id, + String name, + String description, + Project project, + Status status +) { + public enum Status { + NEW, FIXED, TESTED, CLOSED + } +} diff --git a/src/main/java/org/lab/model/Milestone.java b/src/main/java/org/lab/model/Milestone.java new file mode 100644 index 0000000..0d14a50 --- /dev/null +++ b/src/main/java/org/lab/model/Milestone.java @@ -0,0 +1,29 @@ +package org.lab.model; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +public record Milestone( + UUID id, + String name, + String description, + Status status, + LocalDateTime startDate, + LocalDateTime endDate, + List tickets +) { + public enum Status { + OPEN, ACTIVE, CLOSED + } + + public Milestone close() { + var allTicketsClosed = tickets.stream().allMatch(it -> it.status() == Ticket.Status.DONE); + + if (!allTicketsClosed) { + throw new RuntimeException("Not all tickets are closed"); + } + + return new Milestone(id, name, description, Status.CLOSED, startDate, endDate, tickets); + } +} diff --git a/src/main/java/org/lab/model/Project.java b/src/main/java/org/lab/model/Project.java new file mode 100644 index 0000000..3416a00 --- /dev/null +++ b/src/main/java/org/lab/model/Project.java @@ -0,0 +1,27 @@ +package org.lab.model; + +import org.lab.model.user.Developer; +import org.lab.model.user.Manager; +import org.lab.model.user.TeamLead; +import org.lab.model.user.Tester; + +import java.util.List; + +public record Project( + String name, + TeamLead teamLead, + List developers, + List testers, + Manager manager, + Milestone milestone, + List bugReports +) { + + public Project withTeamLead(TeamLead teamLead) { + return new Project(name, teamLead, developers, testers, manager, milestone, bugReports); + } + + public Project withNewMilestone(Milestone milestone) { + return new Project(name, teamLead, developers, testers, manager, milestone, bugReports); + } +} diff --git a/src/main/java/org/lab/model/Ticket.java b/src/main/java/org/lab/model/Ticket.java new file mode 100644 index 0000000..7e402f0 --- /dev/null +++ b/src/main/java/org/lab/model/Ticket.java @@ -0,0 +1,20 @@ +package org.lab.model; + +import org.lab.model.user.Developer; + +import java.util.List; +import java.util.UUID; + +public record Ticket( + UUID id, + String name, + Status status, + List developers, + Project project, + Milestone milestone +) { + + public enum Status { + NEW, ACCEPTED, IN_PROGRESS, DONE + } +} diff --git a/src/main/java/org/lab/model/storage/Storage.java b/src/main/java/org/lab/model/storage/Storage.java new file mode 100644 index 0000000..35a6dad --- /dev/null +++ b/src/main/java/org/lab/model/storage/Storage.java @@ -0,0 +1,364 @@ +package org.lab.model.storage; + +import org.lab.model.BugReport; +import org.lab.model.Milestone; +import org.lab.model.Project; +import org.lab.model.Ticket; +import org.lab.model.user.Developer; +import org.lab.model.user.Manager; +import org.lab.model.user.TeamLead; +import org.lab.model.user.Tester; +import org.lab.model.user.User; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.stream.Gatherer; +import java.util.stream.Stream; + +public class Storage { + private final List projects = new ArrayList<>(); + private final List users = new ArrayList<>(); + private final List tickets = new ArrayList<>(); + private final List bugReports = new ArrayList<>(); + private final List milestones = new ArrayList<>(); + + public void addUser(User user) { + users.add(user); + } + + public List getAllProjectsByUser(User user) { + return projects.stream() + .filter(it -> + it.teamLead().equals(user) || + it.developers().contains(user) || + it.testers().contains(user) || + it.manager().equals(user) + ) + .toList(); + } + + public List getAllTicketsByUser(User user) { + return tickets.stream() + .filter(it -> + it.developers().contains(user) || + it.project().teamLead().equals(user) || + it.project().manager().equals(user) || + it.project().testers().contains(user) + ).toList(); + } + + public List getAllBugsByUser(User user) { + return bugReports.stream() + .filter(it -> + it.project().teamLead().equals(user) || + it.project().manager().equals(user) || + it.project().testers().contains(user) || + it.project().developers().contains(user) + ).toList(); + } + + public void addProject(Project project) { + projects.add(project); + } + + public Project setTeamLeadToProject(User manager, Project project, TeamLead newTeamLead) { + if (!(manager instanceof Manager)) { + throw new IllegalArgumentException("Only a Manager can assign a TeamLead."); + } + + if (!project.manager().equals(manager)) { + throw new IllegalArgumentException("The provided manager is not the project's manager."); + } + + return new Project( + project.name(), + newTeamLead, + project.developers(), + project.testers(), + project.manager(), + project.milestone(), + project.bugReports() + ); + } + + public Project addNewDeveloper(User manager, Project project, Developer developer) { + if (!(manager instanceof Manager)) { + throw new IllegalArgumentException("Only a Manager can add devs."); + } + + if (!project.manager().equals(manager)) { + throw new IllegalArgumentException("The provided manager is not the project's manager."); + } + + var newDevelopers = Stream.concat(project.developers().stream(), Stream.of(developer)) + .toList(); + + return new Project( + project.name(), + project.teamLead(), + newDevelopers, + project.testers(), + project.manager(), + project.milestone(), + project.bugReports() + ); + } + + public Project addNewTester(User manager, Project project, Tester tester) { + if (!(manager instanceof Manager)) { + throw new IllegalArgumentException("Only a Manager can add devs."); + } + + if (!project.manager().equals(manager)) { + throw new IllegalArgumentException("The provided manager is not the project's manager."); + } + + var newTesters = Stream.concat(project.testers().stream(), Stream.of(tester)) + .toList(); + + return new Project( + project.name(), + project.teamLead(), + project.developers(), + newTesters, + project.manager(), + project.milestone(), + project.bugReports() + ); + } + + public Milestone createMilestone(String name, String description, LocalDateTime startDate, LocalDateTime endDate) { + var milestone = new Milestone( + UUID.randomUUID(), + name, + description, + Milestone.Status.OPEN, + startDate, + endDate, + new ArrayList<>() + ); + milestones.add(milestone); + return milestone; + } + + public Project assignMilestoneToProject(User manager, Project project, Milestone milestone) { + if (!(manager instanceof Manager)) { + throw new IllegalArgumentException(""" + Only a Manager can assign a Milestone. + """ + ); + } + + if (!project.manager().equals(manager)) { + throw new IllegalArgumentException("The provided manager is not the project's manager."); + } + + return new Project( + project.name(), + project.teamLead(), + project.developers(), + project.testers(), + project.manager(), + milestone, + project.bugReports() + ); + } + + public boolean canCreateTicket(User user) { + return switch (user) { + case Manager(_), TeamLead(_) -> true; + case Developer(_), Tester(_) -> false; + }; + } + + public Milestone addTicketToMilestone(Ticket ticket, Milestone milestone) { + var updatedMilestone = new Milestone( + milestone.id(), + milestone.name(), + milestone.description(), + milestone.status(), + milestone.startDate(), + milestone.endDate(), + Stream.concat(milestone.tickets().stream(), Stream.of(ticket)).toList() + ); + + milestones.remove(milestone); + milestones.add(updatedMilestone); + + return updatedMilestone; + } + + public List getTicketsWithNoDevelopers(Project project) { + return project.milestone().tickets().stream() + .filter(t -> t.developers() instanceof List devs && devs.isEmpty()) + .toList(); + } + + public Ticket createTicket(String name, Project project, Milestone milestone) { + var ticket = new Ticket( + UUID.randomUUID(), + name, + Ticket.Status.NEW, + List.of(), + project, + milestone + ); + tickets.add(ticket); + return ticket; + } + + public Ticket assignDeveloperToTicket(User user, Ticket ticket, Developer developer) { + if (!(user instanceof Manager || user instanceof TeamLead)) { + throw new IllegalArgumentException("Only Manager or TeamLead can assign developers."); + } + + if (!tickets.contains(ticket)) { + throw new IllegalArgumentException("Ticket does not exist in storage."); + } + + var newDevelopers = new ArrayList<>(ticket.developers()); + newDevelopers.add(developer); + + var updatedTicket = new Ticket( + ticket.id(), + ticket.name(), + Ticket.Status.ACCEPTED, + newDevelopers, + ticket.project(), + ticket.milestone() + ); + + tickets.remove(ticket); + tickets.add(updatedTicket); + + return updatedTicket; + } + + public Ticket completeTicket(User user, Ticket ticket) { + if (!(user instanceof Manager || user instanceof TeamLead)) { + throw new IllegalArgumentException("Only Manager or TeamLead can complete a ticket."); + } + + if (!tickets.contains(ticket)) { + throw new IllegalArgumentException("Ticket does not exist in storage."); + } + + if (ticket.status() != Ticket.Status.IN_PROGRESS) { + throw new IllegalStateException("Ticket must be in IN_PROGRESS status."); + } + + var updatedTicket = new Ticket( + ticket.id(), + ticket.name(), + Ticket.Status.DONE, + ticket.developers(), + ticket.project(), + ticket.milestone() + ); + + tickets.remove(ticket); + tickets.add(updatedTicket); + + return updatedTicket; + } + + public Ticket startWorkingOnTicket(Developer developer, Ticket ticket) { + if (!ticket.developers().contains(developer)) { + throw new IllegalArgumentException("You are not assigned to this ticket."); + } + + if (!tickets.contains(ticket)) { + throw new IllegalArgumentException("Ticket does not exist in storage."); + } + + if (ticket.status() == Ticket.Status.NEW || ticket.status() == Ticket.Status.ACCEPTED) { + return updateTicketStatus(ticket, Ticket.Status.IN_PROGRESS); + } else if (ticket.status() == Ticket.Status.IN_PROGRESS) { + return updateTicketStatus(ticket, Ticket.Status.DONE); + } else { + throw new IllegalStateException("Cannot progress ticket with status: " + ticket.status()); + } + } + + private Ticket updateTicketStatus(Ticket ticket, Ticket.Status newStatus) { + var updatedTicket = new Ticket( + ticket.id(), + ticket.name(), + newStatus, + ticket.developers(), + ticket.project(), + ticket.milestone() + ); + + tickets.remove(ticket); + tickets.add(updatedTicket); + + return updatedTicket; + } + + public BugReport reportBug(String name, String description, Project project) { + var bugReport = new BugReport( + UUID.randomUUID(), + name, + description, + project, + BugReport.Status.NEW + ); + bugReports.add(bugReport); + return bugReport; + } + + public BugReport fixBug(Developer developer, BugReport bugReport) { + if (!(bugReport.project().developers().contains(developer))) { + throw new IllegalArgumentException("You are not part of the project to fix this bug."); + } + + if (!bugReports.contains(bugReport)) { + throw new IllegalArgumentException("Bug report not found."); + } + + var updatedBug = new BugReport( + bugReport.id(), + bugReport.name(), + bugReport.description(), + bugReport.project(), + BugReport.Status.FIXED + ); + + bugReports.remove(bugReport); + bugReports.add(updatedBug); + + return updatedBug; + } + + public BugReport testBugFix(Tester tester, BugReport bugReport) { + if (!bugReport.project().testers().contains(tester)) { + throw new IllegalArgumentException("You are not part of the project to test this bug."); + } + + if (!bugReports.contains(bugReport)) { + throw new IllegalArgumentException("Bug report not found."); + } + + if (bugReport.status() != BugReport.Status.FIXED) { + throw new IllegalStateException("Bug must be fixed before testing."); + } + + var updatedBug = new BugReport( + bugReport.id(), + bugReport.name(), + bugReport.description(), + bugReport.project(), + BugReport.Status.TESTED + ); + + bugReports.remove(bugReport); + bugReports.add(updatedBug); + + return updatedBug; + } +} diff --git a/src/main/java/org/lab/model/user/Developer.java b/src/main/java/org/lab/model/user/Developer.java new file mode 100644 index 0000000..55acf46 --- /dev/null +++ b/src/main/java/org/lab/model/user/Developer.java @@ -0,0 +1,6 @@ +package org.lab.model.user; + +import java.util.UUID; + +public record Developer(UUID id) implements User { +} diff --git a/src/main/java/org/lab/model/user/Manager.java b/src/main/java/org/lab/model/user/Manager.java new file mode 100644 index 0000000..214aa9f --- /dev/null +++ b/src/main/java/org/lab/model/user/Manager.java @@ -0,0 +1,6 @@ +package org.lab.model.user; + +import java.util.UUID; + +public record Manager(UUID id) implements User { +} diff --git a/src/main/java/org/lab/model/user/TeamLead.java b/src/main/java/org/lab/model/user/TeamLead.java new file mode 100644 index 0000000..0f71188 --- /dev/null +++ b/src/main/java/org/lab/model/user/TeamLead.java @@ -0,0 +1,6 @@ +package org.lab.model.user; + +import java.util.UUID; + +public record TeamLead(UUID id) implements User { +} diff --git a/src/main/java/org/lab/model/user/Tester.java b/src/main/java/org/lab/model/user/Tester.java new file mode 100644 index 0000000..9d9c0d7 --- /dev/null +++ b/src/main/java/org/lab/model/user/Tester.java @@ -0,0 +1,6 @@ +package org.lab.model.user; + +import java.util.UUID; + +public record Tester(UUID id) implements User { +} diff --git a/src/main/java/org/lab/model/user/User.java b/src/main/java/org/lab/model/user/User.java new file mode 100644 index 0000000..fa9bbbd --- /dev/null +++ b/src/main/java/org/lab/model/user/User.java @@ -0,0 +1,7 @@ +package org.lab.model.user; + +import java.util.UUID; + +public sealed interface User permits Developer, Manager, TeamLead, Tester { + UUID id(); +}