Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

# Цели и задачи л/р:
Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
73 changes: 70 additions & 3 deletions src/main/java/org/lab/Main.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,71 @@
void main() {
IO.println("Hello and welcome!");
}
package org.lab;

import java.time.LocalDate;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.lab.service.MilestoneService;
import org.lab.service.ProjectRepository;
import org.lab.service.ProjectService;
import org.lab.service.TicketService;
import org.lab.service.UserService;

public class Main {
public static void main(String[] args) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {

Future<ProjectRepository> dbInitTask = executor.submit(() -> {
System.out.println("Инициализация базы данных...");

try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("База данных инициализирована");
return new ProjectRepository();
});

executor.submit(() -> {
try {
System.out.println("Ожидание базы данных...");
var repo = dbInitTask.get();

System.out.println("База данных готова. Приложение запущено");

var projectService = new ProjectService(repo);
var userService = new UserService();
var testUser = userService.register("Manager Viktor");
var testProj = projectService.createProject("Viktor's project", testUser);
var testProjId = testProj.id();
System.out.println("Создан проект '" + testProj.name() + "'");
System.out.println("Роль администратора: " + projectService.getRoleDescription(testProj.id(),
testUser.id()));

var milestoneService = new MilestoneService(repo);
milestoneService.createMilestone(testProjId,
"First milestone",
LocalDate.now(),
LocalDate.ofYearDay(2026, 2),
testUser);
var firstMilestone = projectService.getProject(testProjId).milestones().getFirst();
var firstMilestoneId = firstMilestone.id();
System.out.println("Создан milestone " + firstMilestone.name());

var ticketService = new TicketService(repo);
ticketService.createTicket(testProjId,
firstMilestoneId,
"First ticket",
"Ticket desc",
testUser);
var firstTicket = projectService.getProject(testProjId).milestones().getFirst().tickets().getFirst();
System.out.println("Создан ticket " + firstTicket.title());
} catch (ExecutionException | InterruptedException e) {
System.err.println("Ошибка в приложении: " + e.getMessage());
}
});

}
}
}
12 changes: 12 additions & 0 deletions src/main/java/org/lab/model/BugReport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.lab.model;

public record BugReport(
Long id,
String title,
String description,
Long projectId,
BugStatus status) {
public BugReport withStatus(BugStatus newStatus) {
return new BugReport(id, title, description, projectId, newStatus);
}
}
5 changes: 5 additions & 0 deletions src/main/java/org/lab/model/BugStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.lab.model;

public enum BugStatus {
NEW, FIXED, TESTED, CLOSED
}
17 changes: 17 additions & 0 deletions src/main/java/org/lab/model/Milestone.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.lab.model;

import java.time.LocalDate;
import java.util.List;

public record Milestone(
Long id,
String name,
LocalDate startDate,
LocalDate endDate,
Long projectId,
MilestoneStatus status,
List<Ticket> tickets) {
public Milestone {
tickets = tickets == null ? List.of() : List.copyOf(tickets);
}
}
5 changes: 5 additions & 0 deletions src/main/java/org/lab/model/MilestoneStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.lab.model;

public enum MilestoneStatus {
OPEN, ACTIVE, CLOSED
}
25 changes: 25 additions & 0 deletions src/main/java/org/lab/model/Project.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.lab.model;

import java.util.List;
import java.util.Optional;

public record Project(
Long id,
String name,
User manager,
Optional<User> teamLead,
List<User> developers,
List<User> testers,
List<Milestone> milestones,
List<BugReport> bugReports) {
public Project {
developers = developers == null ? List.of() : List.copyOf(developers);
testers = testers == null ? List.of() : List.copyOf(testers);
milestones = milestones == null ? List.of() : List.copyOf(milestones);
bugReports = bugReports == null ? List.of() : List.copyOf(bugReports);
}

public Project withMilestone(Milestone milestone) {
return this;
}
}
15 changes: 15 additions & 0 deletions src/main/java/org/lab/model/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.lab.model;

public sealed interface Role permits Role.Manager, Role.TeamLead, Role.Developer, Role.Tester {
record Manager() implements Role {
}

record TeamLead() implements Role {
}

record Developer() implements Role {
}

record Tester() implements Role {
}
}
22 changes: 22 additions & 0 deletions src/main/java/org/lab/model/Ticket.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.lab.model;

import java.util.Optional;

public record Ticket(
Long id,
String title,
String description,
Long projectId,
Long milestoneId,
Optional<User> assignee,
Optional<User> creator,
TicketStatus status) {
public Ticket withStatus(TicketStatus newStatus) {
return new Ticket(id, title, description, projectId, milestoneId, assignee, creator, newStatus);
}

public Ticket withAssignee(User newAssignee) {
return new Ticket(id, title, description, projectId, milestoneId, Optional.ofNullable(newAssignee), creator,
status);
}
}
5 changes: 5 additions & 0 deletions src/main/java/org/lab/model/TicketStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.lab.model;

public enum TicketStatus {
NEW, ACCEPTED, IN_PROGRESS, COMPLETED
}
4 changes: 4 additions & 0 deletions src/main/java/org/lab/model/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.lab.model;

public record User(Long id, String name) {
}
22 changes: 22 additions & 0 deletions src/main/java/org/lab/service/AuthorizationUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.lab.service;

import org.lab.model.Project;
import org.lab.model.User;

public class AuthorizationUtils {
public static boolean isManager(Project p, User u) {
return p.manager().id().equals(u.id());
}

public static boolean isTeamLead(Project p, User u) {
return p.teamLead().isPresent() && p.teamLead().get().id().equals(u.id());
}

public static boolean isDeveloper(Project p, User u) {
return p.developers().stream().anyMatch(d -> d.id().equals(u.id()));
}

public static boolean isTester(Project p, User u) {
return p.testers().stream().anyMatch(t -> t.id().equals(u.id()));
}
}
63 changes: 63 additions & 0 deletions src/main/java/org/lab/service/BugReportService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.lab.service;

import org.lab.model.BugReport;
import org.lab.model.BugStatus;
import org.lab.model.Project;
import org.lab.model.User;

import java.util.ArrayList;
import java.util.List;

import static org.lab.service.AuthorizationUtils.isDeveloper;
import static org.lab.service.AuthorizationUtils.isTester;

public class BugReportService {
private final ProjectRepository repository;

public BugReportService(ProjectRepository repository) {
this.repository = repository;
}

public List<BugReport> getBugReportsForUser(User user) {
return repository.findAll().stream()
.filter(p -> p.developers().stream().anyMatch(d -> d.id().equals(user.id())))
.flatMap(p -> p.bugReports().stream())
.filter(b -> b.status() == BugStatus.NEW)
.toList();
}

public void createBugReport(Long projectId, String title, String description, User creator) {
repository.findById(projectId).ifPresent(project -> {
BugReport report = new BugReport(repository.generateId(), title, description, projectId,
BugStatus.NEW);
List<BugReport> newReports = new ArrayList<>(project.bugReports());
newReports.add(report);
repository.save(new Project(project.id(), project.name(), project.manager(), project.teamLead(),
project.developers(), project.testers(), project.milestones(), newReports));
});
}

public void updateBugStatus(Long projectId, Long bugId, BugStatus newStatus, User user) {
repository.findById(projectId).ifPresent(project -> {
if (isDeveloper(project, user)) {
if (newStatus != BugStatus.FIXED)
throw new IllegalArgumentException("Разработчик может пометить баг только как FIXED");
} else if (isTester(project, user)) {
if (newStatus != BugStatus.TESTED && newStatus != BugStatus.CLOSED)
throw new IllegalArgumentException("Тестировщик может пометить баг только как TESTED или CLOSED");
} else {
throw new IllegalArgumentException("Только разработчик или тестировщик может менять статус бага");
}

List<BugReport> newReports = project.bugReports().stream()
.map(b -> {
if (b.id().equals(bugId)) {
return b.withStatus(newStatus);
}
return b;
}).toList();
repository.save(new Project(project.id(), project.name(), project.manager(), project.teamLead(),
project.developers(), project.testers(), project.milestones(), newReports));
});
}
}
67 changes: 67 additions & 0 deletions src/main/java/org/lab/service/MilestoneService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.lab.service;

import org.lab.model.*;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

import static org.lab.service.AuthorizationUtils.isManager;

public class MilestoneService {
private final ProjectRepository repository;

public MilestoneService(ProjectRepository repository) {
this.repository = repository;
}

public void createMilestone(Long projectId, String name, LocalDate start, LocalDate end, User creator) {
repository.findById(projectId).ifPresent(project -> {
if (!isManager(project, creator)) {
throw new IllegalArgumentException("Только менеджер может создавать milestone");
}
Milestone milestone = new Milestone(repository.generateId(), name, start, end, projectId,
MilestoneStatus.OPEN, List.of());
List<Milestone> newMilestones = new ArrayList<>(project.milestones());
newMilestones.add(milestone);
repository.save(new Project(project.id(), project.name(), project.manager(), project.teamLead(),
project.developers(), project.testers(), newMilestones, project.bugReports()));
});
}

public void updateMilestoneStatus(Long projectId, Long milestoneId, MilestoneStatus newStatus, User user) {
repository.findById(projectId).ifPresent(project -> {
if (!isManager(project, user)) {
throw new IllegalArgumentException("Только менеджер может менять статус milestone");
}

if (newStatus == MilestoneStatus.ACTIVE) {
boolean hasActive = project.milestones().stream()
.anyMatch(m -> m.status() == MilestoneStatus.ACTIVE && !m.id().equals(milestoneId));
if (hasActive) {
throw new IllegalStateException(
"Проект уже имеет активный milestone. Закройте или деактивируйте его.");
}
}

List<Milestone> newMilestones = project.milestones().stream()
.map(m -> {
if (m.id().equals(milestoneId)) {
if (newStatus == MilestoneStatus.CLOSED) {
boolean allTicketsCompleted = m.tickets().stream()
.allMatch(t -> t.status() == TicketStatus.COMPLETED);
if (!allTicketsCompleted) {
throw new IllegalStateException(
"Нельзя закрыть milestone с незавершенными тикетами");
}
}
return new Milestone(m.id(), m.name(), m.startDate(), m.endDate(), m.projectId(), newStatus,
m.tickets());
}
return m;
}).toList();
repository.save(new Project(project.id(), project.name(), project.manager(), project.teamLead(),
project.developers(), project.testers(), newMilestones, project.bugReports()));
});
}
}
30 changes: 30 additions & 0 deletions src/main/java/org/lab/service/ProjectRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.lab.service;

import org.lab.model.Project;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

public class ProjectRepository {
private final Map<Long, Project> projects = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);

public Project save(Project project) {
projects.put(project.id(), project);
return project;
}

public Optional<Project> findById(Long id) {
return Optional.ofNullable(projects.get(id));
}

public long generateId() {
return idGenerator.getAndIncrement();
}

public Collection<Project> findAll() {
return projects.values();
}
}
Loading