diff --git a/.gitignore b/.gitignore index b63da45..bf3e1b2 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,5 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store +/.idea/ diff --git a/README.md b/README.md index 4a80115..aeb9b42 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/TvkQWWs6) # Features of modern Java # Цели и задачи л/р: На основе индивидуального задания произвести разработку бизнес-логики бэкэнда entriprise-системы. В ходе реализации необходимо использовать возможности современных версий языка Java: -* Pattern matching для switch -* строковые шаблоны)))))))))))))) -* расширенные возможности стандартной библиотеки Java -* sealed классы и record -* программирование в функциональном стиле -* preview как project Valhalla, structured concurrency... -* и т.д. +* Pattern matching для switch - TicketService.getTaskDescription() +* sealed классы - Task, Ticket, BugReport +* record - ManagementSystem +* программирование в функциональном стиле - Stream API и лямбды +* preview как structured concurrency - BugReportService.findBugReportsToFix() +* compact source files # Обязательное условие: * Использование системы сборки Gradle diff --git a/build.gradle.kts b/build.gradle.kts index 79bf52a..147752e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,12 +9,26 @@ repositories { mavenCentral() } +java { + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 +} + + dependencies { + implementation("org.projectlombok:lombok:1.18.42") + compileOnly("org.projectlombok:lombok:1.18.42") + annotationProcessor("org.projectlombok:lombok:1.18.42") testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } +tasks.withType { + options.compilerArgs.add("--enable-preview") + options.compilerArgs.add("-parameters") +} tasks.test { useJUnitPlatform() + jvmArgs("--enable-preview") } \ No newline at end of file diff --git a/src/main/java/org/lab/Main.java b/src/main/java/org/lab/Main.java index 22028ef..eee9de5 100644 --- a/src/main/java/org/lab/Main.java +++ b/src/main/java/org/lab/Main.java @@ -1,4 +1,95 @@ +import org.lab.ManagementSystem; +import org.lab.model.BugReport; +import org.lab.model.Milestone; +import org.lab.model.MilestoneStatus; +import org.lab.model.Project; +import org.lab.model.Ticket; +import org.lab.model.User; +import org.lab.service.BugReportService; +import org.lab.service.MilestoneService; +import org.lab.service.ProjectService; +import org.lab.service.TicketService; +import org.lab.service.UserService; + void main() { - IO.println("Hello and welcome!"); -} + ManagementSystem system = ManagementSystem.defaults(); + + UserService userService = system.userService(); + ProjectService projectService = system.projectService(); + BugReportService bugReportService = system.bugReportService(); + MilestoneService milestoneService = system.milestoneService(); + TicketService ticketService = system.ticketService(); + + User manager = userService.registerUser("manager", "m"); + System.out.println("Registered manager: " + manager); + + User developer1 = userService.registerUser("developer1", "d"); + System.out.println("Registered developer1: " + developer1); + + User developer2 = userService.registerUser("developer2", "d"); + System.out.println("Registered developer2: " + developer2); + + User tester = userService.registerUser("tester", "t"); + System.out.println("Registered tester: " + tester); + + User teamLead = userService.registerUser("teamLead", "t"); + System.out.println("Registered teamLead: " + teamLead); + + Project project = projectService.createProject(manager); + System.out.println("Created project: " + project); + + projectService.assignTeamLeader(manager, project.getId(), teamLead); + System.out.println("Assigned team leader to project"); + + projectService.addDeveloperToProject(manager, project.getId(), developer1); + System.out.println("Added developer1 to project"); + + projectService.addDeveloperToProject(manager, project.getId(), developer2); + System.out.println("Added developer2 to project"); + + projectService.addTesterToProject(manager, project.getId(), tester); + System.out.println("Added tester to project"); + + Milestone milestone = milestoneService.createMilestone( + manager, + LocalDate.now(), + LocalDate.now().plusDays(2), + project + ); + System.out.println("Created milestone: " + milestone); + + milestoneService.changeMilestoneStatus(manager, project.getId(), milestone.getId(), MilestoneStatus.ACTIVE); + System.out.println("Changed milestone status to ACTIVE " + milestone); + + Ticket ticket = ticketService.createTicketForProject(teamLead, milestone.getId()); + System.out.println("Created ticket: " + ticket); + + ticketService.assignDeveloperToTicket(manager, ticket.getId(), developer2); + System.out.println("Assigned developer2 to ticket"); + + ticketService.executeTicket(developer2, ticket.getId()); + System.out.println("Executed ticket (first time)"); + + ticketService.executeTicket(developer2, ticket.getId()); + System.out.println("Executed ticket (second time)"); + + boolean ticketCompletion = ticketService.checkTicketCompletion(teamLead, ticket.getId()); + System.out.println("Ticket completion status: " + ticketCompletion); + + BugReport bugReport = bugReportService.testProject(tester, project.getId()); + System.out.println("Created bug report from testing: " + bugReport); + + Set fixedBugReports = bugReportService.findBugReportsToFix(developer1).stream() + .map(bug -> bugReportService.fixBugReport(developer1, bug.getId())) + .collect(Collectors.toSet()); + System.out.println("Fixed bug reports: " + fixedBugReports); + + Stream verifiedReports = fixedBugReports.stream() + .map(report -> bugReportService.verifyBugFix(tester, report.getId(), true)); + System.out.println("Verifying bug reports..."); + verifiedReports.forEach(report -> { + bugReportService.closeBugReport(manager, report.getId()); + System.out.println("Closed bug report: " + report); + }); +} \ No newline at end of file diff --git a/src/main/java/org/lab/ManagementSystem.java b/src/main/java/org/lab/ManagementSystem.java new file mode 100644 index 0000000..49e475e --- /dev/null +++ b/src/main/java/org/lab/ManagementSystem.java @@ -0,0 +1,64 @@ +package org.lab; + +import org.lab.repository.BugReportRepository; +import org.lab.repository.MilestoneRepository; +import org.lab.repository.ProjectRepository; +import org.lab.repository.TicketRepository; +import org.lab.repository.UserRepository; +import org.lab.service.BugReportService; +import org.lab.service.MilestoneService; +import org.lab.service.ProjectService; +import org.lab.service.TicketService; +import org.lab.service.UserRoleValidationService; +import org.lab.service.UserService; + +public record ManagementSystem( + BugReportService bugReportService, + MilestoneService milestoneService, + ProjectService projectService, + TicketService ticketService, + UserService userService, + UserRoleValidationService userRoleValidationService +) { + public static ManagementSystem defaults() { + BugReportRepository bugReportRepository = new BugReportRepository(); + MilestoneRepository milestoneRepository = new MilestoneRepository(); + ProjectRepository projectRepository = new ProjectRepository(); + TicketRepository ticketRepository = new TicketRepository(); + UserRepository userRepository = new UserRepository(); + + UserRoleValidationService userRoleValidationService = new UserRoleValidationService(projectRepository); + + ProjectService projectService = new ProjectService( + projectRepository, + userRoleValidationService + ); + MilestoneService milestoneService = new MilestoneService( + milestoneRepository, + projectService, + ticketRepository, + userRoleValidationService + ); + return new ManagementSystem( + new BugReportService( + bugReportRepository, + projectRepository, + userRoleValidationService + ), + milestoneService, + projectService, + new TicketService( + ticketRepository, + projectService, + milestoneService, + userRoleValidationService + ), + new UserService( + userRepository, + projectRepository, + ticketRepository + ), + userRoleValidationService + ); + } +} 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..7db215e --- /dev/null +++ b/src/main/java/org/lab/model/BugReport.java @@ -0,0 +1,14 @@ +package org.lab.model; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public final class BugReport implements Task { + private Long id; + private long projectId; + private BugReportStatus status; +} diff --git a/src/main/java/org/lab/model/BugReportStatus.java b/src/main/java/org/lab/model/BugReportStatus.java new file mode 100644 index 0000000..3f897c1 --- /dev/null +++ b/src/main/java/org/lab/model/BugReportStatus.java @@ -0,0 +1,8 @@ +package org.lab.model; + +public enum BugReportStatus { + 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..65cc66a --- /dev/null +++ b/src/main/java/org/lab/model/Milestone.java @@ -0,0 +1,19 @@ +package org.lab.model; + +import java.time.LocalDate; +import java.util.Set; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class Milestone { + private Long id; + private MilestoneStatus status; + private LocalDate startDate; + private LocalDate endDate; + private Set tickets; +} diff --git a/src/main/java/org/lab/model/MilestoneStatus.java b/src/main/java/org/lab/model/MilestoneStatus.java new file mode 100644 index 0000000..fe4dae4 --- /dev/null +++ b/src/main/java/org/lab/model/MilestoneStatus.java @@ -0,0 +1,7 @@ +package org.lab.model; + +public enum MilestoneStatus { + OPEN, + ACTIVE, + CLOSED, +} 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..d500e60 --- /dev/null +++ b/src/main/java/org/lab/model/Project.java @@ -0,0 +1,19 @@ +package org.lab.model; + +import java.util.Set; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class Project { + private Long id; + private Set developers; + private Set testers; + private User manager; + private User teamLeader; + private Set milestones; +} diff --git a/src/main/java/org/lab/model/Role.java b/src/main/java/org/lab/model/Role.java new file mode 100644 index 0000000..11bf2e2 --- /dev/null +++ b/src/main/java/org/lab/model/Role.java @@ -0,0 +1,8 @@ +package org.lab.model; + +public enum Role { + MANAGER, + TEAMLEAD, + DEVELOPER, + QA, +} diff --git a/src/main/java/org/lab/model/Task.java b/src/main/java/org/lab/model/Task.java new file mode 100644 index 0000000..2df2670 --- /dev/null +++ b/src/main/java/org/lab/model/Task.java @@ -0,0 +1,5 @@ +package org.lab.model; + +public sealed interface Task permits Ticket, BugReport { + Long getId(); +} 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..c1830f6 --- /dev/null +++ b/src/main/java/org/lab/model/Ticket.java @@ -0,0 +1,17 @@ +package org.lab.model; + +import java.util.Set; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public final class Ticket implements Task { + private Long id; + private long milestoneId; + private TicketStatus status; + private Set assignees; +} diff --git a/src/main/java/org/lab/model/TicketStatus.java b/src/main/java/org/lab/model/TicketStatus.java new file mode 100644 index 0000000..fa60cba --- /dev/null +++ b/src/main/java/org/lab/model/TicketStatus.java @@ -0,0 +1,8 @@ +package org.lab.model; + +public enum TicketStatus { + NEW, + ACCEPTED, + IN_PROGRESS, + COMPLETED, +} diff --git a/src/main/java/org/lab/model/User.java b/src/main/java/org/lab/model/User.java new file mode 100644 index 0000000..42c7843 --- /dev/null +++ b/src/main/java/org/lab/model/User.java @@ -0,0 +1,16 @@ +package org.lab.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@RequiredArgsConstructor +public class User { + private Long id; + private String login; + private String password; +} diff --git a/src/main/java/org/lab/repository/BugReportRepository.java b/src/main/java/org/lab/repository/BugReportRepository.java new file mode 100644 index 0000000..55e695e --- /dev/null +++ b/src/main/java/org/lab/repository/BugReportRepository.java @@ -0,0 +1,34 @@ +package org.lab.repository; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.List; +import java.util.stream.Collectors; + +import org.lab.model.BugReport; + +public class BugReportRepository { + private final Map bugReports = new ConcurrentHashMap<>(); + private final AtomicLong idGenerator = new AtomicLong(1); + + public BugReport save(BugReport bugReport) { + if (bugReport.getId() == null) { + bugReport.setId(idGenerator.getAndIncrement()); + } + bugReports.put(bugReport.getId(), bugReport); + return bugReport; + } + + public Optional findById(Long id) { + return Optional.ofNullable(bugReports.get(id)); + } + + public List findByProjectId(Long projectId) { + return bugReports.values().stream() + .filter(bugReport -> Objects.equals(bugReport.getProjectId(), projectId)) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/org/lab/repository/MilestoneRepository.java b/src/main/java/org/lab/repository/MilestoneRepository.java new file mode 100644 index 0000000..d649d84 --- /dev/null +++ b/src/main/java/org/lab/repository/MilestoneRepository.java @@ -0,0 +1,46 @@ +package org.lab.repository; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.List; +import java.util.stream.Collectors; + +import org.lab.model.Milestone; +import org.lab.model.MilestoneStatus; + +public class MilestoneRepository { + private final Map milestones = new ConcurrentHashMap<>(); + private final AtomicLong idGenerator = new AtomicLong(1); + + public Milestone save(Milestone milestone) { + if (milestone.getId() == null) { + milestone.setId(idGenerator.getAndIncrement()); + } + milestones.put(milestone.getId(), milestone); + return milestone; + } + + public Optional findById(Long id) { + return Optional.ofNullable(milestones.get(id)); + } + + public List findByStatus(MilestoneStatus status) { + return milestones.values().stream() + .filter(milestone -> milestone.getStatus().equals(status)) + .collect(Collectors.toList()); + } + + public List findAll() { + return milestones.values().stream().collect(Collectors.toList()); + } + + public boolean existsById(Long id) { + return milestones.containsKey(id); + } + + public void deleteById(Long id) { + milestones.remove(id); + } +} \ No newline at end of file diff --git a/src/main/java/org/lab/repository/ProjectRepository.java b/src/main/java/org/lab/repository/ProjectRepository.java new file mode 100644 index 0000000..f0bcb62 --- /dev/null +++ b/src/main/java/org/lab/repository/ProjectRepository.java @@ -0,0 +1,53 @@ +package org.lab.repository; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.List; +import java.util.stream.Collectors; + +import org.lab.model.Project; +import org.lab.model.User; + +public class ProjectRepository { + private final Map projects = new ConcurrentHashMap<>(); + private final AtomicLong idGenerator = new AtomicLong(1); + + public Project save(Project project) { + if (project.getId() == null) { + project.setId(idGenerator.getAndIncrement()); + } + projects.put(project.getId(), project); + return project; + } + + public Optional findById(Long id) { + return Optional.ofNullable(projects.get(id)); + } + + public List findByManager(User manager) { + return projects.values().stream() + .filter(project -> project.getManager().equals(manager)) + .collect(Collectors.toList()); + } + + public List findByTeamLeader(User teamLeader) { + return projects.values().stream() + .filter(project -> project.getTeamLeader().equals(teamLeader)) + .collect(Collectors.toList()); + } + + public List findAll() { + return new ArrayList<>(projects.values()); + } + + public boolean existsById(Long id) { + return projects.containsKey(id); + } + + public void deleteById(Long id) { + projects.remove(id); + } +} \ No newline at end of file diff --git a/src/main/java/org/lab/repository/TicketRepository.java b/src/main/java/org/lab/repository/TicketRepository.java new file mode 100644 index 0000000..4d6a875 --- /dev/null +++ b/src/main/java/org/lab/repository/TicketRepository.java @@ -0,0 +1,54 @@ +package org.lab.repository; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.List; +import java.util.stream.Collectors; + +import org.lab.model.Ticket; +import org.lab.model.TicketStatus; + +public class TicketRepository { + private final Map tickets = new ConcurrentHashMap<>(); + private final AtomicLong idGenerator = new AtomicLong(1); + + public Ticket save(Ticket ticket) { + if (ticket.getId() == null) { + ticket.setId(idGenerator.getAndIncrement()); + } + tickets.put(ticket.getId(), ticket); + return ticket; + } + + public Optional findById(Long id) { + return Optional.ofNullable(tickets.get(id)); + } + + public List findByMilestoneId(Long milestoneId) { + return tickets.values().stream() + .filter(ticket -> Objects.equals(ticket.getMilestoneId(), milestoneId)) + .collect(Collectors.toList()); + } + + public List findByStatus(TicketStatus status) { + return tickets.values().stream() + .filter(ticket -> ticket.getStatus().equals(status)) + .collect(Collectors.toList()); + } + + public List findAll() { + return new ArrayList<>(tickets.values()); + } + + public boolean existsById(Long id) { + return tickets.containsKey(id); + } + + public void deleteById(Long id) { + tickets.remove(id); + } +} \ No newline at end of file diff --git a/src/main/java/org/lab/repository/UserRepository.java b/src/main/java/org/lab/repository/UserRepository.java new file mode 100644 index 0000000..c476d27 --- /dev/null +++ b/src/main/java/org/lab/repository/UserRepository.java @@ -0,0 +1,30 @@ +package org.lab.repository; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import org.lab.model.User; + +public class UserRepository { + private final Map users = new ConcurrentHashMap<>(); + private final AtomicLong idGenerator = new AtomicLong(1); + + public User save(User user) { + if (user.getId() == null) { + user.setId(idGenerator.getAndIncrement()); + } + users.put(user.getId(), user); + return user; + } + + public Optional findById(Long id) { + return Optional.ofNullable(users.get(id)); + } + + public boolean existsByLogin(String login) { + return users.values().stream() + .anyMatch(user -> user.getLogin().equals(login)); + } +} diff --git a/src/main/java/org/lab/service/BugReportService.java b/src/main/java/org/lab/service/BugReportService.java new file mode 100644 index 0000000..3523a1a --- /dev/null +++ b/src/main/java/org/lab/service/BugReportService.java @@ -0,0 +1,121 @@ +package org.lab.service; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.StructuredTaskScope; +import java.util.stream.Collectors; + +import lombok.RequiredArgsConstructor; +import org.lab.model.BugReport; +import org.lab.model.BugReportStatus; +import org.lab.model.Role; +import org.lab.model.User; +import org.lab.repository.BugReportRepository; +import org.lab.repository.ProjectRepository; + +@RequiredArgsConstructor +public class BugReportService { + + private final BugReportRepository bugReportRepository; + private final ProjectRepository projectRepository; + private final UserRoleValidationService userRoleValidationService; + + public BugReport findById(Long id) { + return bugReportRepository.findById(id).orElse(null); + } + + public Set findBugReportsToFix(User user) { + try (var scope = StructuredTaskScope.open()) { + List>> tasks = projectRepository.findAll().stream() + .filter(project -> project.getDevelopers().contains(user)) + .map(project -> scope.fork(() -> + bugReportRepository.findByProjectId(project.getId()))) + .toList(); + + scope.join(); + + return tasks.stream() + .map(StructuredTaskScope.Subtask::get) + .flatMap(List::stream) + .filter(bugReport -> BugReportStatus.NEW.equals(bugReport.getStatus())) + .collect(Collectors.toSet()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Bug search interrupted", e); + } + } + + public BugReport createBugReportForProject(User user, Long projectId) { + userRoleValidationService.validateUserHasRoles(user, projectId, Role.DEVELOPER, Role.QA); + + if (projectId == null) { + throw new IllegalArgumentException("Project ID cannot be null"); + } + + BugReport bugReport = new BugReport(); + bugReport.setProjectId(projectId); + bugReport.setStatus(BugReportStatus.NEW); + + bugReportRepository.save(bugReport); + return bugReport; + } + + public BugReport fixBugReport(User developer, Long bugReportId) { + BugReport bugReport = findById(bugReportId); + if (bugReport == null) { + throw new IllegalArgumentException("Bug report with ID '" + bugReportId + "' does not exist"); + } + + userRoleValidationService.validateUserHasRoles(developer, bugReport.getProjectId(), Role.DEVELOPER); + + if (!BugReportStatus.NEW.equals(bugReport.getStatus()) && !BugReportStatus.TESTED.equals(bugReport.getStatus())) { + throw new IllegalStateException("Bug report cannot be fixed in current status: " + bugReport.getStatus()); + } + + bugReport.setStatus(BugReportStatus.FIXED); + return bugReport; + } + + public BugReport testProject(User tester, Long projectId) { + userRoleValidationService.validateUserHasRoles(tester, projectId, Role.QA); + + return createBugReportForProject(tester, projectId); + } + + public BugReport verifyBugFix(User tester, Long bugReportId, boolean isFixed) { + BugReport bugReport = findById(bugReportId); + if (bugReport == null) { + throw new IllegalArgumentException("Bug report with ID '" + bugReportId + "' does not exist"); + } + + userRoleValidationService.validateUserHasRoles(tester, bugReport.getProjectId(), Role.QA); + + if (!BugReportStatus.FIXED.equals(bugReport.getStatus())) { + throw new IllegalStateException("Bug report must be in FIXED status to verify. Current status: " + bugReport.getStatus()); + } + + if (isFixed) { + bugReport.setStatus(BugReportStatus.TESTED); + } else { + bugReport.setStatus(BugReportStatus.NEW); + } + + return bugReport; + } + + public BugReport closeBugReport(User tester, Long bugReportId) { + BugReport bugReport = findById(bugReportId); + if (bugReport == null) { + throw new IllegalArgumentException("Bug report with ID '" + bugReportId + "' does not exist"); + } + + userRoleValidationService.validateUserHasRoles(tester, bugReport.getProjectId(), Role.MANAGER); + + if (!BugReportStatus.TESTED.equals(bugReport.getStatus())) { + throw new IllegalStateException("Bug report must be tested before closing. Current status: " + bugReport.getStatus()); + } + + bugReport.setStatus(BugReportStatus.CLOSED); + return bugReport; + } +} \ No newline at end of file diff --git a/src/main/java/org/lab/service/MilestoneService.java b/src/main/java/org/lab/service/MilestoneService.java new file mode 100644 index 0000000..ea615e9 --- /dev/null +++ b/src/main/java/org/lab/service/MilestoneService.java @@ -0,0 +1,66 @@ +package org.lab.service; + +import java.time.LocalDate; +import java.util.Objects; + +import lombok.RequiredArgsConstructor; +import org.lab.model.Milestone; +import org.lab.model.MilestoneStatus; +import org.lab.model.Project; +import org.lab.model.Role; +import org.lab.model.TicketStatus; +import org.lab.model.User; +import org.lab.repository.MilestoneRepository; +import org.lab.repository.TicketRepository; + +@RequiredArgsConstructor +public class MilestoneService { + + private final MilestoneRepository milestoneRepository; + private final ProjectService projectService; + private final TicketRepository ticketRepository; + private final UserRoleValidationService userRoleValidationService; + + public Milestone createMilestone(User manager, LocalDate startDate, LocalDate endDate, Project project) { + userRoleValidationService.validateUserHasRoles(manager, project.getId(), Role.MANAGER); + + if (startDate.isAfter(endDate)) { + throw new IllegalArgumentException("Start date cannot be after end date"); + } + + Milestone milestone = new Milestone(); + milestone.setStatus(MilestoneStatus.OPEN); + milestone.setStartDate(startDate); + milestone.setEndDate(endDate); + + project.getMilestones().add(milestone); + + return milestoneRepository.save(milestone); + } + + public Milestone findById(Long id) { + return milestoneRepository.findById(id).orElse(null); + } + + public Milestone changeMilestoneStatus(User manager, Long projectId, Long milestoneId, MilestoneStatus newStatus) { + userRoleValidationService.validateUserHasRoles(manager, projectId, Role.MANAGER); + + Milestone milestone = findById(milestoneId); + if (newStatus == MilestoneStatus.CLOSED) { + validateTicketsClosed(milestone); + } + + milestone.setStatus(newStatus); + return milestone; + } + + private void validateTicketsClosed(Milestone milestone) { + milestone.getTickets().stream() + .filter(ticket -> Objects.equals(ticket.getMilestoneId(), milestone.getId())) + .forEach(ticket -> { + if (!ticket.getStatus().equals(TicketStatus.COMPLETED)) { + throw new IllegalArgumentException("Ticket with ID '" + ticket.getId() + "' is not completed"); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/org/lab/service/ProjectService.java b/src/main/java/org/lab/service/ProjectService.java new file mode 100644 index 0000000..f0062ae --- /dev/null +++ b/src/main/java/org/lab/service/ProjectService.java @@ -0,0 +1,90 @@ +package org.lab.service; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.RequiredArgsConstructor; +import org.lab.model.Project; +import org.lab.model.Role; +import org.lab.model.User; +import org.lab.repository.ProjectRepository; + +@RequiredArgsConstructor +public class ProjectService { + + private final ProjectRepository projectRepository; + private final UserRoleValidationService userRoleValidationService; + + public Project createProject(User manager) { + if (manager == null) { + throw new IllegalArgumentException("Manager cannot be null"); + } + + Project project = new Project(); + project.setManager(manager); + project.setDevelopers(new HashSet<>()); + project.setTesters(new HashSet<>()); + project.setMilestones(new HashSet<>()); + + return projectRepository.save(project); + } + + public Project findById(Long id) { + return projectRepository.findById(id).orElse(null); + } + + public Set findInvolvedProjects(User user) { + return projectRepository.findAll().stream() + .filter(project -> isMember(user, project)) + .collect(Collectors.toSet()); + } + + public List findAll() { + return projectRepository.findAll(); + } + + public Project assignTeamLeader(User manager, Long projectId, User teamLeader) { + Project project = findById(projectId); + if (project == null) { + throw new IllegalArgumentException("Project with ID '" + projectId + "' does not exist"); + } + + userRoleValidationService.validateUserHasRoles(manager, project.getId(), Role.MANAGER); + + project.setTeamLeader(teamLeader); + return project; + } + + public Project addDeveloperToProject(User manager, Long projectId, User developer) { + Project project = findById(projectId); + if (project == null) { + throw new IllegalArgumentException("Project with ID '" + projectId + "' does not exist"); + } + + userRoleValidationService.validateUserHasRoles(manager, project.getId(), Role.MANAGER); + + project.getDevelopers().add(developer); + return project; + } + + public Project addTesterToProject(User manager, Long projectId, User tester) { + Project project = findById(projectId); + if (project == null) { + throw new IllegalArgumentException("Project with ID '" + projectId + "' does not exist"); + } + + userRoleValidationService.validateUserHasRoles(manager, project.getId(), Role.MANAGER); + + project.getTesters().add(tester); + return project; + } + + private boolean isMember(User user, Project project) { + return project.getManager() == user || + project.getTeamLeader() == user || + project.getDevelopers().contains(user) || + project.getTesters().contains(user); + } +} \ No newline at end of file diff --git a/src/main/java/org/lab/service/TicketService.java b/src/main/java/org/lab/service/TicketService.java new file mode 100644 index 0000000..6607f64 --- /dev/null +++ b/src/main/java/org/lab/service/TicketService.java @@ -0,0 +1,115 @@ +package org.lab.service; + +import java.util.HashSet; + +import lombok.RequiredArgsConstructor; +import org.lab.model.BugReport; +import org.lab.model.Milestone; +import org.lab.model.Project; +import org.lab.model.Role; +import org.lab.model.Task; +import org.lab.model.Ticket; +import org.lab.model.TicketStatus; +import org.lab.model.User; +import org.lab.repository.TicketRepository; + +@RequiredArgsConstructor +public class TicketService { + + private final TicketRepository ticketRepository; + private final ProjectService projectService; + private final MilestoneService milestoneService; + private final UserRoleValidationService userRoleValidationService; + + public Ticket findById(Long id) { + return ticketRepository.findById(id).orElse(null); + } + + public String getTaskDescription(Task task) { + return switch (task) { + case BugReport bug -> "Bug Report #" + bug.getId() + " - Status: " + bug.getStatus(); + case Ticket ticket -> "Ticket #" + ticket.getId() + " - Status: " + ticket.getStatus() + + " - Assignees: " + ticket.getAssignees().size(); + }; + } + + + private Long getProjectIdFromMilestone(Long milestoneId) { + Milestone milestone = milestoneService.findById(milestoneId); + if (milestone == null) { + throw new IllegalArgumentException("Milestone with ID '" + milestoneId + "' does not exist"); + } + + return projectService.findAll().stream() + .filter(project -> project.getMilestones().contains(milestone)) + .findFirst() + .map(Project::getId) + .orElseThrow(() -> new IllegalArgumentException("Milestone with ID '" + milestoneId + "' does not " + + "exist in any project")); + } + + public Ticket createTicketForProject(User user, Long milestoneId) { + Long projectId = getProjectIdFromMilestone(milestoneId); + userRoleValidationService.validateUserHasRoles(user, projectId, Role.MANAGER, Role.TEAMLEAD); + + Ticket ticket = new Ticket(); + ticket.setMilestoneId(milestoneId); + ticket.setStatus(TicketStatus.NEW); + ticket.setAssignees(new HashSet<>()); + + Ticket savedTicket = ticketRepository.save(ticket); + + Milestone milestone = milestoneService.findById(milestoneId); + if (milestone.getTickets() == null) { + milestone.setTickets(new HashSet<>()); + } + milestone.getTickets().add(savedTicket); + + return savedTicket; + } + + public Ticket assignDeveloperToTicket(User user, Long ticketId, User developer) { + Ticket ticket = findById(ticketId); + if (ticket == null) { + throw new IllegalArgumentException("Ticket with ID '" + ticketId + "' does not exist"); + } + + Long projectId = getProjectIdFromMilestone(ticket.getMilestoneId()); + userRoleValidationService.validateUserHasRoles(user, projectId, Role.MANAGER, Role.TEAMLEAD); + + Project project = projectService.findById(projectId); + if (!project.getDevelopers().contains(developer)) { + throw new IllegalArgumentException("User is not a developer in this project"); + } + + ticket.getAssignees().add(developer); + return ticketRepository.save(ticket); + } + + public boolean checkTicketCompletion(User user, Long ticketId) { + Ticket ticket = findById(ticketId); + + Long projectId = getProjectIdFromMilestone(ticket.getMilestoneId()); + userRoleValidationService.validateUserHasRoles(user, projectId, Role.MANAGER, Role.TEAMLEAD); + + return TicketStatus.COMPLETED.equals(ticket.getStatus()); + } + + public Ticket executeTicket(User developer, Long ticketId) { + Ticket ticket = findById(ticketId); + Long projectId = getProjectIdFromMilestone(ticket.getMilestoneId()); + userRoleValidationService.validateUserHasRoles(developer, projectId, Role.DEVELOPER); + + if (!ticket.getAssignees().contains(developer)) { + throw new SecurityException("Developer is not assigned to this ticket"); + } + + if (TicketStatus.NEW.equals(ticket.getStatus()) || TicketStatus.ACCEPTED.equals(ticket.getStatus())) { + ticket.setStatus(TicketStatus.IN_PROGRESS); + } else if (TicketStatus.IN_PROGRESS.equals(ticket.getStatus())) { + ticket.setStatus(TicketStatus.COMPLETED); + } + + return ticket; + } +} \ No newline at end of file diff --git a/src/main/java/org/lab/service/UserRoleValidationService.java b/src/main/java/org/lab/service/UserRoleValidationService.java new file mode 100644 index 0000000..f55856c --- /dev/null +++ b/src/main/java/org/lab/service/UserRoleValidationService.java @@ -0,0 +1,42 @@ +package org.lab.service; + +import java.util.Arrays; + +import lombok.RequiredArgsConstructor; +import org.lab.model.Project; +import org.lab.model.Role; +import org.lab.model.User; +import org.lab.repository.ProjectRepository; + +@RequiredArgsConstructor +public class UserRoleValidationService { + private final ProjectRepository projectRepository; + + public void validateUserHasRoles(User user, Long projectId, Role... roles) { + Project project = projectRepository.findById(projectId).orElseThrow(() -> + new IllegalArgumentException("Project with ID '" + projectId + "' does not exist") + ); + boolean anyMatch = Arrays.stream(roles).anyMatch(role -> role.equals(getUserRole(user, project))); + if (!anyMatch) { + throw new SecurityException("User does not have required roles"); + } + } + + public Role getUserRole(User user, Project project) { + if (project.getManager() != null && project.getManager().equals(user)) { + return Role.MANAGER; + } + if (project.getTeamLeader() != null && project.getTeamLeader().equals(user)) { + return Role.TEAMLEAD; + } + if (project.getDevelopers().contains(user)) { + return Role.DEVELOPER; + } + if (project.getTesters().contains(user)) { + return Role.QA; + } + return null; + } + + +} diff --git a/src/main/java/org/lab/service/UserService.java b/src/main/java/org/lab/service/UserService.java new file mode 100644 index 0000000..5093ee2 --- /dev/null +++ b/src/main/java/org/lab/service/UserService.java @@ -0,0 +1,39 @@ +package org.lab.service; + +import lombok.RequiredArgsConstructor; +import org.lab.model.User; +import org.lab.repository.ProjectRepository; +import org.lab.repository.TicketRepository; +import org.lab.repository.UserRepository; + +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + private final ProjectRepository projectRepository; + private final TicketRepository ticketRepository; + + public User registerUser(String login, String password) { + if (userRepository.existsByLogin(login)) { + throw new IllegalArgumentException("User with login '" + login + "' already exists"); + } + + if (login == null || login.trim().isEmpty()) { + throw new IllegalArgumentException("Login cannot be empty"); + } + + if (password == null || password.trim().isEmpty()) { + throw new IllegalArgumentException("Password cannot be empty"); + } + + User user = new User(); + user.setLogin(login.trim()); + user.setPassword(password); + + return userRepository.save(user); + } + + public User findById(Long id) { + return userRepository.findById(id).orElse(null); + } +} \ No newline at end of file