diff --git a/.github/workflows/generate-plantuml.yml b/.github/workflows/generate-plantuml.yml new file mode 100644 index 000000000..d623f481e --- /dev/null +++ b/.github/workflows/generate-plantuml.yml @@ -0,0 +1,44 @@ +name: Generate PlantUML Diagrams + +on: + push: + branches: + - master + +permissions: + contents: write + +jobs: + generate-diagrams: + name: Generate and Commit Diagrams + runs-on: ubuntu-latest + if: github.actor != 'github-actions[bot]' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + cache: maven + + - name: Generate PlantUML diagrams + working-directory: MtdrSpring/backend + run: sh ./mvnw -q -Parchitecture-diagrams -DskipTests process-classes + + - name: Commit generated diagrams + run: | + if git status --porcelain docs/diagrams | grep -q '^'; then + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add docs/diagrams/*.puml + git commit -m "docs: generate PlantUML diagrams [skip ci]" + git push origin HEAD:${GITHUB_REF_NAME} + else + echo "No diagram changes to commit." + fi \ No newline at end of file diff --git a/MtdrSpring/backend/pom.xml b/MtdrSpring/backend/pom.xml index b87b3da6a..d91f4a0d6 100644 --- a/MtdrSpring/backend/pom.xml +++ b/MtdrSpring/backend/pom.xml @@ -19,6 +19,9 @@ 1.5.5.Final 0.12.6 ${project.basedir}/src/main/frontend + ${project.basedir}/../../docs/diagrams + ${project.basedir}/src/main/java + com.springboot.MyTodoList v23.9.0 8.1.2 false @@ -208,4 +211,46 @@ + + + + architecture-diagrams + + + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + + + generate-plantuml-diagrams + process-classes + + java + + + com.springboot.MyTodoList.architecture.PlantUmlDiagramGenerator + compile + + + architecture.sourceDir + ${architecture.source-dir} + + + architecture.outputDir + ${architecture.output-dir} + + + architecture.basePackage + ${architecture.base-package} + + + + + + + + + + diff --git a/MtdrSpring/backend/src/main/frontend/package-lock.json b/MtdrSpring/backend/src/main/frontend/package-lock.json index 7ad4ddd47..9b089ee29 100644 --- a/MtdrSpring/backend/src/main/frontend/package-lock.json +++ b/MtdrSpring/backend/src/main/frontend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "chuvabot-frontend", + "name": "kairobot-frontend", "version": "0.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "chuvabot-frontend", + "name": "kairobot-frontend", "version": "0.1.0", "dependencies": { "react": "^18.2.0", diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/architecture/PlantUmlDiagramGenerator.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/architecture/PlantUmlDiagramGenerator.java new file mode 100644 index 000000000..b228df6e2 --- /dev/null +++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/architecture/PlantUmlDiagramGenerator.java @@ -0,0 +1,430 @@ +package com.springboot.MyTodoList.architecture; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Generates PlantUML class diagrams from the Java source tree. + * + *

The generator keeps the implementation intentionally lightweight so it can run as part of the + * Maven build without introducing extra parsing dependencies. It groups classes into package/component + * diagrams and writes one .puml file per group.

+ */ +public final class PlantUmlDiagramGenerator { + + private static final Pattern PACKAGE_PATTERN = Pattern.compile("^\\s*package\\s+([\\w.]+)\\s*;"); + private static final Pattern TYPE_PATTERN = Pattern.compile( + "^\\s*(?:@[\\w.()\\\"',\\s]+\\s+)*(?:(?:public|protected|private|abstract|final|static|sealed|non-sealed)\\s+)*(class|interface|enum|record)\\s+(\\w+)\\b(.*)$"); + private static final Set JAVA_MODIFIERS = Set.of( + "public", "protected", "private", "static", "final", "abstract", "transient", "volatile", + "synchronized", "default", "native", "strictfp", "sealed", "non-sealed" + ); + private static final Set IGNORE_TYPE_TOKENS = Set.of( + "String", "Object", "Integer", "Long", "Short", "Byte", "Double", "Float", "Boolean", "Character", + "List", "Map", "Set", "Collection", "Iterable", "Optional", "Stream", "Enum", "Date", "LocalDate", + "LocalDateTime", "Instant", "BigDecimal", "BigInteger", "UUID", "Class", "Path", "IOException" + ); + + private PlantUmlDiagramGenerator() { + } + + public static void main(String[] args) throws IOException { + Path sourceDir = Path.of(System.getProperty("architecture.sourceDir", "src/main/java")); + Path outputDir = Path.of(System.getProperty("architecture.outputDir", "../../docs/diagrams")); + String basePackage = System.getProperty("architecture.basePackage", "com.springboot.MyTodoList"); + + List types = scanTypes(sourceDir, basePackage); + Map typesBySimpleName = indexBySimpleName(types); + Map> categorizedTypes = categorize(types); + + Files.createDirectories(outputDir); + for (DiagramCategory category : DiagramCategory.values()) { + Path diagramPath = outputDir.resolve(category.fileName); + List categoryTypes = categorizedTypes.getOrDefault(category, List.of()); + if (categoryTypes.isEmpty()) { + Files.deleteIfExists(diagramPath); + continue; + } + + writeDiagram(diagramPath, category, categoryTypes, typesBySimpleName); + } + } + + private static List scanTypes(Path sourceDir, String basePackage) throws IOException { + if (!Files.exists(sourceDir)) { + return List.of(); + } + + List types = new ArrayList<>(); + try (var paths = Files.walk(sourceDir)) { + for (Path path : paths.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".java")).sorted().toList()) { + TypeInfo typeInfo = parseType(path, basePackage); + if (typeInfo != null) { + types.add(typeInfo); + } + } + } + return types; + } + + private static TypeInfo parseType(Path file, String basePackage) throws IOException { + List lines = Files.readAllLines(file, StandardCharsets.UTF_8); + String packageName = null; + int typeStartIndex = -1; + String kind = null; + String simpleName = null; + String declarationLine = null; + + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + if (packageName == null) { + Matcher packageMatcher = PACKAGE_PATTERN.matcher(line); + if (packageMatcher.find()) { + packageName = packageMatcher.group(1); + continue; + } + } + + Matcher typeMatcher = TYPE_PATTERN.matcher(line); + if (typeMatcher.find()) { + kind = typeMatcher.group(1); + simpleName = typeMatcher.group(2); + declarationLine = line.trim(); + typeStartIndex = i; + break; + } + } + + if (packageName == null || typeStartIndex < 0 || simpleName == null || !packageName.startsWith(basePackage) || packageName.startsWith(basePackage + ".architecture")) { + return null; + } + + List members = new ArrayList<>(); + Set dependencyTypes = new LinkedHashSet<>(); + StringBuilder body = new StringBuilder(); + int depth = 0; + boolean typeBodyStarted = false; + for (int i = typeStartIndex; i < lines.size(); i++) { + String line = lines.get(i); + body.append(line).append(System.lineSeparator()); + depth += count(line, '{'); + depth -= count(line, '}'); + if (line.contains("{")) { + typeBodyStarted = true; + } + + if (!typeBodyStarted || depth < 1) { + continue; + } + + if (i == typeStartIndex) { + dependencyTypes.addAll(extractTypeTokens(extractDeclarationTail(declarationLine))); + continue; + } + + if (depth == 1) { + MemberInfo member = parseMember(line); + if (member != null) { + members.add(member); + dependencyTypes.addAll(extractTypeTokens(member.typeExpression)); + } + } + + if (depth == 0) { + break; + } + } + + return new TypeInfo(packageName, simpleName, kind, members, dependencyTypes, file); + } + + private static MemberInfo parseMember(String line) { + String trimmed = line.trim(); + if (trimmed.isEmpty() || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) { + return null; + } + if (!trimmed.endsWith(";") || trimmed.contains("(") || trimmed.startsWith("return ")) { + return null; + } + + String declaration = trimmed.substring(0, trimmed.length() - 1).trim(); + declaration = removeLeadingAnnotations(declaration); + declaration = removeLeadingModifiers(declaration); + if (declaration.isEmpty() || declaration.contains("{") || declaration.contains("}") || declaration.contains("\"") || declaration.startsWith("@")) { + return null; + } + + int assignmentIndex = declaration.indexOf('='); + if (assignmentIndex >= 0) { + declaration = declaration.substring(0, assignmentIndex).trim(); + } + + int lastSpace = declaration.lastIndexOf(' '); + if (lastSpace < 1) { + return null; + } + + String typeExpression = declaration.substring(0, lastSpace).trim(); + String name = declaration.substring(lastSpace + 1).trim(); + if (typeExpression.isEmpty() || name.isEmpty()) { + return null; + } + + return new MemberInfo(name, typeExpression); + } + + private static String removeLeadingAnnotations(String value) { + String result = value.trim(); + while (result.startsWith("@")) { + int spaceIndex = result.indexOf(' '); + if (spaceIndex < 0) { + return ""; + } + result = result.substring(spaceIndex + 1).trim(); + } + return result; + } + + private static String removeLeadingModifiers(String value) { + String result = value.trim(); + boolean changed; + do { + changed = false; + for (String modifier : JAVA_MODIFIERS) { + String prefix = modifier + " "; + if (result.startsWith(prefix)) { + result = result.substring(prefix.length()).trim(); + changed = true; + } + } + } while (changed); + return result; + } + + private static String extractDeclarationTail(String declarationLine) { + if (declarationLine == null) { + return ""; + } + int typeIndex = declarationLine.indexOf("class "); + if (typeIndex < 0) { + typeIndex = declarationLine.indexOf("interface "); + } + if (typeIndex < 0) { + typeIndex = declarationLine.indexOf("enum "); + } + if (typeIndex < 0) { + typeIndex = declarationLine.indexOf("record "); + } + return typeIndex < 0 ? declarationLine : declarationLine.substring(typeIndex); + } + + private static Set extractTypeTokens(String expression) { + if (expression == null || expression.isBlank()) { + return Set.of(); + } + + String cleaned = expression + .replaceAll("/\\*.*?\\*/", " ") + .replaceAll("<[^>]*>", " ") + .replace('[', ' ') + .replace(']', ' ') + .replace('?', ' ') + .replace('&', ' ') + .replace('|', ' ') + .replace(',', ' ') + .replace("extends", " ") + .replace("super", " "); + + Set tokens = new LinkedHashSet<>(); + for (String token : cleaned.split("[^A-Za-z0-9_$.]+")) { + if (token.isBlank()) { + continue; + } + String simpleToken = token.contains(".") ? token.substring(token.lastIndexOf('.') + 1) : token; + if (simpleToken.isBlank()) { + continue; + } + if (Character.isUpperCase(simpleToken.charAt(0)) && !IGNORE_TYPE_TOKENS.contains(simpleToken)) { + tokens.add(simpleToken); + } + } + return tokens; + } + + private static Map indexBySimpleName(List types) { + Map index = new LinkedHashMap<>(); + for (TypeInfo type : types) { + index.putIfAbsent(type.simpleName(), type); + } + return index; + } + + private static Map> categorize(List types) { + Map> categorized = new EnumMap<>(DiagramCategory.class); + for (DiagramCategory category : DiagramCategory.values()) { + categorized.put(category, new ArrayList<>()); + } + + for (TypeInfo type : types) { + EnumSet categories = classify(type); + for (DiagramCategory category : categories) { + categorized.get(category).add(type); + } + } + + for (Map.Entry> entry : categorized.entrySet()) { + entry.getValue().sort((left, right) -> left.simpleName().compareToIgnoreCase(right.simpleName())); + } + return categorized; + } + + private static EnumSet classify(TypeInfo type) { + EnumSet categories = EnumSet.noneOf(DiagramCategory.class); + String packageName = type.packageName().toLowerCase(Locale.ROOT); + String simpleName = type.simpleName(); + String lowerSimpleName = simpleName.toLowerCase(Locale.ROOT); + + if (packageName.contains("controller") || lowerSimpleName.endsWith("controller") || lowerSimpleName.equals("spacontroller")) { + categories.add(DiagramCategory.CONTROLLERS); + } + if (packageName.contains("repository") || lowerSimpleName.endsWith("repository")) { + categories.add(DiagramCategory.REPOSITORIES); + } + if (packageName.contains("service") || lowerSimpleName.endsWith("service") || lowerSimpleName.endsWith("scheduler")) { + categories.add(DiagramCategory.SERVICES); + } + if (packageName.contains("notification") || lowerSimpleName.startsWith("notification")) { + categories.add(DiagramCategory.NOTIFICATION); + } + if (packageName.contains("telegram.nlu") || packageName.endsWith(".nlu") || lowerSimpleName.contains("nlu") || simpleName.equals("NaturalLanguageRouter")) { + categories.add(DiagramCategory.NLU); + } + if ((packageName.contains(".telegram") && !packageName.contains(".telegram.nlu")) || lowerSimpleName.endsWith("bot")) { + categories.add(DiagramCategory.BOT); + } + if (packageName.contains("config") || lowerSimpleName.endsWith("config") || lowerSimpleName.endsWith("filter") || lowerSimpleName.endsWith("util") || lowerSimpleName.endsWith("detailsserviceimpl")) { + categories.add(DiagramCategory.CONFIG); + } + if (packageName.contains("audit") || lowerSimpleName.startsWith("audit")) { + categories.add(DiagramCategory.AUDIT); + } + if (categories.isEmpty() && !lowerSimpleName.endsWith("application")) { + categories.add(DiagramCategory.MODELS); + } + + // Keep domain and shared model classes in the models diagram even if they are also cross-cutting concerns. + if (categories.contains(DiagramCategory.AUDIT) || categories.contains(DiagramCategory.CONFIG)) { + categories.add(DiagramCategory.MODELS); + } + + return categories; + } + + private static void writeDiagram(Path diagramPath, DiagramCategory category, List types, Map typesBySimpleName) throws IOException { + Set includedSimpleNames = types.stream().map(TypeInfo::simpleName).collect(Collectors.toCollection(TreeSet::new)); + StringBuilder diagram = new StringBuilder(); + diagram.append("@startuml\n"); + diagram.append("title ").append(category.title).append("\n"); + diagram.append("skinparam classAttributeIconSize 0\n"); + diagram.append("hide empty members\n"); + diagram.append("left to right direction\n\n"); + + diagram.append("package \"").append(category.title).append("\" {\n"); + for (TypeInfo type : types) { + diagram.append(renderClass(type)).append('\n'); + } + diagram.append("}\n\n"); + + Set relationLines = new LinkedHashSet<>(); + for (TypeInfo type : types) { + for (String dependency : type.dependencyTypes()) { + TypeInfo resolved = typesBySimpleName.get(dependency); + if (resolved == null || resolved.simpleName().equals(type.simpleName())) { + continue; + } + String relation = type.simpleName() + " --> " + resolved.simpleName(); + if (includedSimpleNames.contains(resolved.simpleName())) { + relation += " : uses"; + } else { + relation += " : depends on"; + } + relationLines.add(relation); + } + } + + if (!relationLines.isEmpty()) { + diagram.append(String.join("\n", relationLines)).append('\n'); + } + diagram.append("@enduml\n"); + + Files.writeString(diagramPath, diagram.toString(), StandardCharsets.UTF_8); + } + + private static String renderClass(TypeInfo type) { + StringBuilder rendered = new StringBuilder(); + rendered.append(type.kind()).append(' ').append(type.simpleName()).append(" {\n"); + for (MemberInfo member : type.members()) { + rendered.append(" + ").append(member.name()).append(" : ").append(member.typeExpression()).append("\n"); + } + rendered.append("}\n"); + return rendered.toString(); + } + + private static int count(String value, char needle) { + int count = 0; + for (int i = 0; i < value.length(); i++) { + if (value.charAt(i) == needle) { + count++; + } + } + return count; + } + + private record TypeInfo( + String packageName, + String simpleName, + String kind, + List members, + Set dependencyTypes, + Path sourceFile + ) { + } + + private record MemberInfo(String name, String typeExpression) { + } + + private enum DiagramCategory { + CONTROLLERS("controllers.puml", "Controllers"), + SERVICES("services.puml", "Services"), + REPOSITORIES("repositories.puml", "Repositories"), + BOT("bot.puml", "Bot"), + NOTIFICATION("notification.puml", "Notification"), + NLU("nlu.puml", "NLU"), + CONFIG("config.puml", "Config"), + AUDIT("audit.puml", "Audit"), + MODELS("models.puml", "Entities and Models"); + + private final String fileName; + private final String title; + + DiagramCategory(String fileName, String title) { + this.fileName = fileName; + this.title = title; + } + } +} \ No newline at end of file diff --git a/architecture/model.dsl b/architecture/model.dsl index d43a0ebfb..fd070bfd2 100644 --- a/architecture/model.dsl +++ b/architecture/model.dsl @@ -11,17 +11,37 @@ workspace { bot = softwareSystem "Oracle Java Bot Platform" "Automates task management, NLU-based chat interactions, and integrations." { webApp = container "Web App" "Task/project dashboard." "React.js" - telegramBot = container "Telegram Bot Adapter" "Handles message and chat routing with NLU workflow." "Node.js" - nlu = container "NLU Service" "Parses and classifies user intent/entities for commands via text/chat." "Java Spring Boot or Python" + telegramBot = container "Telegram Bot Adapter" "Handles message and chat routing with NLU workflow." "Node.js" { + url "https://github.com/PatWhite29/Oracle_Java_Bot/blob/master/docs/diagrams/bot.puml" + } + nlu = container "NLU Service" "Parses and classifies user intent/entities for commands via text/chat." "Java Spring Boot or Python" { + url "https://github.com/PatWhite29/Oracle_Java_Bot/blob/master/docs/diagrams/nlu.puml" + } backend = container "Spring Boot Backend" "Skill orchestration, data/services, notifications, and security." "Java, Spring Boot" { - userController = component "User Controller" "User CRUD, authZ/authN." "REST Controller" - taskController = component "Task Controller" "CRUD for tasks, status management, assignment." "REST Controller" - sprintController = component "Sprint Controller" "Sprint planning/execution." "REST Controller" - projectService = component "Project Service" "Business/project logic." "Spring Service" - taskService = component "Task Service" "Task rules, assignment, priorities." "Spring Service" - notificationService = component "Notification Service" "User notifications via web/Telegram." "Spring Service" - repositoryLayer = component "Repository Layer" "Repository for all system entities." "Spring Data/JPA" - dbAccess = component "Database Access Layer" "Performs SQL/PLSQL DB connection." "JDBC/Spring Data" + userController = component "User Controller" "User CRUD, authZ/authN." "REST Controller" { + url "https://github.com/PatWhite29/Oracle_Java_Bot/blob/master/docs/diagrams/controllers.puml" + } + taskController = component "Task Controller" "CRUD for tasks, status management, assignment." "REST Controller" { + url "https://github.com/PatWhite29/Oracle_Java_Bot/blob/master/docs/diagrams/controllers.puml" + } + sprintController = component "Sprint Controller" "Sprint planning/execution." "REST Controller" { + url "https://github.com/PatWhite29/Oracle_Java_Bot/blob/master/docs/diagrams/controllers.puml" + } + projectService = component "Project Service" "Business/project logic." "Spring Service" { + url "https://github.com/PatWhite29/Oracle_Java_Bot/blob/master/docs/diagrams/services.puml" + } + taskService = component "Task Service" "Task rules, assignment, priorities." "Spring Service" { + url "https://github.com/PatWhite29/Oracle_Java_Bot/blob/master/docs/diagrams/services.puml" + } + notificationService = component "Notification Service" "User notifications via web/Telegram." "Spring Service" { + url "https://github.com/PatWhite29/Oracle_Java_Bot/blob/master/docs/diagrams/notification.puml" + } + repositoryLayer = component "Repository Layer" "Repository for all system entities." "Spring Data/JPA" { + url "https://github.com/PatWhite29/Oracle_Java_Bot/blob/master/docs/diagrams/repositories.puml" + } + dbAccess = component "Database Access Layer" "Performs SQL/PLSQL DB connection." "JDBC/Spring Data" { + url "https://github.com/PatWhite29/Oracle_Java_Bot/blob/master/docs/diagrams/repositories.puml" + } } db = container "Oracle Autonomous Database" "Persistence for users, tasks, sprints, projects, logs." "Oracle Autonomous DB" { tag "database" diff --git a/docs/diagrams/audit.puml b/docs/diagrams/audit.puml new file mode 100644 index 000000000..87360f664 --- /dev/null +++ b/docs/diagrams/audit.puml @@ -0,0 +1,36 @@ +@startuml +title Audit +skinparam classAttributeIconSize 0 +hide empty members +left to right direction + +package "Audit" { +enum AuditAction { +} + +class AuditLog { + + id : Long + + employee : User + + entityType : EntityType + + entityId : Long + + action : AuditAction + + oldValue : String + + newValue : String + + createdAt : LocalDateTime +} + +interface AuditLogRepository { +} + +class AuditLogService { + + auditLogRepository : AuditLogRepository + + objectMapper : ObjectMapper +} + +} + +AuditLog --> User : depends on +AuditLog --> EntityType : depends on +AuditLog --> AuditAction : uses +AuditLogService --> AuditLogRepository : uses +@enduml diff --git a/docs/diagrams/bot.puml b/docs/diagrams/bot.puml new file mode 100644 index 000000000..0d33c467c --- /dev/null +++ b/docs/diagrams/bot.puml @@ -0,0 +1,89 @@ +@startuml +title Bot +skinparam classAttributeIconSize 0 +hide empty members +left to right direction + +package "Bot" { +class CommentHandler { + + userService : UserService + + activityService : TaskActivityService +} + +class HelpHandler { +} + +class KairoBot { + + botToken : String + + telegramClient : TelegramClient + + startHandler : StartHandler + + loginHandler : LoginHandler + + helpHandler : HelpHandler + + myProjectsHandler : MyProjectsHandler + + myTasksHandler : MyTasksHandler + + taskHandler : TaskHandler + + taskStatusHandler : TaskStatusHandler + + commentHandler : CommentHandler + + nluRouter : NaturalLanguageRouter + + objectMapper : ObjectMapper +} + +class LoginHandler { + + userService : UserService +} + +class MyProjectsHandler { + + userService : UserService + + projectService : ProjectService +} + +class MyTasksHandler { + + userService : UserService + + taskService : TaskService +} + +class StartHandler { +} + +class SyntheticUpdateFactory { +} + +class TaskHandler { + + userService : UserService + + taskService : TaskService +} + +class TaskStatusHandler { + + userService : UserService + + taskService : TaskService +} + +class TelegramBotConfig { +} + +class TelegramHelper { +} + +} + +CommentHandler --> UserService : depends on +CommentHandler --> TaskActivityService : depends on +KairoBot --> StartHandler : uses +KairoBot --> LoginHandler : uses +KairoBot --> HelpHandler : uses +KairoBot --> MyProjectsHandler : uses +KairoBot --> MyTasksHandler : uses +KairoBot --> TaskHandler : uses +KairoBot --> TaskStatusHandler : uses +KairoBot --> CommentHandler : uses +KairoBot --> NaturalLanguageRouter : depends on +LoginHandler --> UserService : depends on +MyProjectsHandler --> UserService : depends on +MyProjectsHandler --> ProjectService : depends on +MyTasksHandler --> UserService : depends on +MyTasksHandler --> TaskService : depends on +TaskHandler --> UserService : depends on +TaskHandler --> TaskService : depends on +TaskStatusHandler --> UserService : depends on +TaskStatusHandler --> TaskService : depends on +@enduml diff --git a/docs/diagrams/config.puml b/docs/diagrams/config.puml new file mode 100644 index 000000000..c553cc569 --- /dev/null +++ b/docs/diagrams/config.puml @@ -0,0 +1,49 @@ +@startuml +title Config +skinparam classAttributeIconSize 0 +hide empty members +left to right direction + +package "Config" { +class CorsConfig { +} + +class JwtAuthFilter { + + jwtUtil : JwtUtil + + userDetailsService : UserDetailsServiceImpl +} + +class JwtUtil { + + signingKey : SecretKey + + expirationMs : long +} + +class RequestLoggingFilter { +} + +class SecurityConfig { + + jwtAuthFilter : JwtAuthFilter + + userDetailsService : UserDetailsServiceImpl +} + +class SpaController { +} + +class SwaggerConfig { +} + +class TelegramBotConfig { +} + +class UserDetailsServiceImpl { + + userRepository : UserRepository +} + +} + +JwtAuthFilter --> JwtUtil : uses +JwtAuthFilter --> UserDetailsServiceImpl : uses +SecurityConfig --> JwtAuthFilter : uses +SecurityConfig --> UserDetailsServiceImpl : uses +UserDetailsServiceImpl --> UserRepository : depends on +@enduml diff --git a/docs/diagrams/controllers.puml b/docs/diagrams/controllers.puml new file mode 100644 index 000000000..4ff181c9f --- /dev/null +++ b/docs/diagrams/controllers.puml @@ -0,0 +1,53 @@ +@startuml +title Controllers +skinparam classAttributeIconSize 0 +hide empty members +left to right direction + +package "Controllers" { +class AuthController { + + authService : AuthService +} + +class DashboardController { + + dashboardService : DashboardService +} + +class ProjectController { + + projectService : ProjectService +} + +class ProjectMemberController { + + memberService : ProjectMemberService +} + +class SpaController { +} + +class SprintController { + + sprintService : SprintService +} + +class TaskActivityController { + + activityService : TaskActivityService +} + +class TaskController { + + taskService : TaskService +} + +class UserController { + + userService : UserService +} + +} + +AuthController --> AuthService : depends on +DashboardController --> DashboardService : depends on +ProjectController --> ProjectService : depends on +ProjectMemberController --> ProjectMemberService : depends on +SprintController --> SprintService : depends on +TaskActivityController --> TaskActivityService : depends on +TaskController --> TaskService : depends on +UserController --> UserService : depends on +@enduml diff --git a/docs/diagrams/models.puml b/docs/diagrams/models.puml new file mode 100644 index 000000000..da9464457 --- /dev/null +++ b/docs/diagrams/models.puml @@ -0,0 +1,421 @@ +@startuml +title Entities and Models +skinparam classAttributeIconSize 0 +hide empty members +left to right direction + +package "Entities and Models" { +enum ActivityType { +} + +class AddMemberRequest { + + userId : Long +} + +enum AuditAction { +} + +class AuditLog { + + id : Long + + employee : User + + entityType : EntityType + + entityId : Long + + action : AuditAction + + oldValue : String + + newValue : String + + createdAt : LocalDateTime +} + +interface AuditLogRepository { +} + +class AuditLogService { + + auditLogRepository : AuditLogRepository + + objectMapper : ObjectMapper +} + +class AuthResponse { + + token : String + + user : UserResponse +} + +class BacklogResponse { + + totalTasks : long + + totalStoryPoints : long + + byPriority : Map +} + +class BurndownResponse { + + sprintId : Long + + sprintName : String + + totalStoryPoints : long + + completedStoryPoints : long + + remainingStoryPoints : long + + idealBurnPercent : double + + actualBurnPercent : double +} + +class ClosedSprintException { +} + +class CloseProjectResponse { + + projectId : Long + + projectName : String + + sprintsClosed : int + + pendingTasks : int + + message : String +} + +class CommentRequest { + + content : String +} + +class ConflictException { +} + +class CorsConfig { +} + +class EfficiencyResponse { + + sprintId : Long + + sprintName : String + + totalActualHours : double + + totalSpCompleted : long + + members : List +} + +enum EntityType { +} + +class ErrorResponse { + + error : String + + message : String + + status : int +} + +class ForbiddenException { +} + +class GlobalExceptionHandler { +} + +class JwtAuthFilter { + + jwtUtil : JwtUtil + + userDetailsService : UserDetailsServiceImpl +} + +class JwtUtil { + + signingKey : SecretKey + + expirationMs : long +} + +class LoginRequest { + + email : String + + password : String +} + +class NotProjectParticipantException { +} + +record PagedResponse { +} + +class Project { + + id : Long + + projectName : String + + description : String + + status : ProjectStatus + + manager : User + + createdAt : LocalDateTime +} + +interface ProjectMapper { +} + +class ProjectMember { + + id : Long + + project : Project + + employee : User + + createdAt : LocalDateTime +} + +interface ProjectMemberMapper { +} + +class ProjectMemberResponse { + + id : Long + + employee : UserSummary + + createdAt : LocalDateTime +} + +class ProjectRequest { + + projectName : String + + description : String +} + +class ProjectResponse { + + id : Long + + projectName : String + + description : String + + status : ProjectStatus + + manager : UserSummary + + createdAt : LocalDateTime +} + +enum ProjectStatus { +} + +class RegisterRequest { + + fullName : String + + email : String + + password : String +} + +class RequestLoggingFilter { +} + +class ResourceNotFoundException { +} + +class SecurityConfig { + + jwtAuthFilter : JwtAuthFilter + + userDetailsService : UserDetailsServiceImpl +} + +class SpaController { +} + +class Sprint { + + id : Long + + project : Project + + sprintName : String + + goal : String + + startDate : LocalDate + + endDate : LocalDate + + status : SprintStatus + + createdAt : LocalDateTime +} + +class SprintChangeRequest { + + sprintId : Long +} + +interface SprintMapper { +} + +class SprintRequest { + + sprintName : String + + goal : String + + startDate : LocalDate + + endDate : LocalDate +} + +class SprintResponse { + + id : Long + + projectId : Long + + sprintName : String + + goal : String + + startDate : LocalDate + + endDate : LocalDate + + status : SprintStatus + + createdAt : LocalDateTime +} + +enum SprintStatus { +} + +class SprintSummary { + + id : Long + + sprintName : String + + status : SprintStatus +} + +class SprintSummaryResponse { + + sprintId : Long + + sprintName : String + + statusCounts : Map + + spCommitted : long + + spCompleted : long + + completionPercentage : double + + blockedCount : int +} + +class StatusChangeRequest { + + status : TaskStatus + + actualHours : BigDecimal +} + +class SwaggerConfig { +} + +class Task { + + id : Long + + project : Project + + sprint : Sprint + + taskName : String + + description : String + + status : TaskStatus + + priority : TaskPriority + + storyPoints : Integer + + assignedTo : User + + createdBy : User + + actualHours : BigDecimal + + createdAt : LocalDateTime +} + +class TaskActivity { + + id : Long + + task : Task + + employee : User + + activityType : ActivityType + + content : String + + createdAt : LocalDateTime +} + +interface TaskActivityMapper { +} + +class TaskActivityResponse { + + id : Long + + taskId : Long + + employee : UserSummary + + activityType : ActivityType + + content : String + + createdAt : LocalDateTime +} + +interface TaskMapper { +} + +enum TaskPriority { +} + +class TaskRequest { + + taskName : String + + description : String + + status : TaskStatus + + priority : TaskPriority + + storyPoints : Integer + + assignedTo : Long + + sprintId : Long +} + +class TaskResponse { + + id : Long + + projectId : Long + + sprintId : Long + + sprint : SprintSummary + + taskName : String + + description : String + + status : TaskStatus + + priority : TaskPriority + + storyPoints : Integer + + assignedTo : UserSummary + + createdBy : UserSummary + + actualHours : BigDecimal + + createdAt : LocalDateTime +} + +enum TaskStatus { +} + +class TelegramBotConfig { +} + +class TransferRequest { + + newManagerId : Long +} + +class User { + + id : Long + + fullName : String + + email : String + + passwordHash : String + + telegramChatId : Long + + isActive : boolean + + createdAt : LocalDateTime +} + +class UserDetailsServiceImpl { + + userRepository : UserRepository +} + +interface UserMapper { +} + +class UserRequest { + + fullName : String + + email : String +} + +class UserResponse { + + id : Long + + fullName : String + + email : String + + telegramChatId : Long + + createdAt : LocalDateTime +} + +class UserSummary { + + id : Long + + fullName : String + + email : String +} + +class ValidationException { +} + +class VelocityResponse { + + sprintId : Long + + sprintName : String + + spCompleted : long +} + +class WorkloadResponse { + + userId : Long + + fullName : String + + taskCounts : Map + + storyPoints : Map +} + +} + +AuditLog --> User : uses +AuditLog --> EntityType : uses +AuditLog --> AuditAction : uses +AuditLogService --> AuditLogRepository : uses +AuthResponse --> UserResponse : uses +JwtAuthFilter --> JwtUtil : uses +JwtAuthFilter --> UserDetailsServiceImpl : uses +Project --> ProjectStatus : uses +Project --> User : uses +ProjectMember --> Project : uses +ProjectMember --> User : uses +ProjectMemberResponse --> UserSummary : uses +ProjectResponse --> ProjectStatus : uses +ProjectResponse --> UserSummary : uses +SecurityConfig --> JwtAuthFilter : uses +SecurityConfig --> UserDetailsServiceImpl : uses +Sprint --> Project : uses +Sprint --> SprintStatus : uses +SprintResponse --> SprintStatus : uses +SprintSummary --> SprintStatus : uses +StatusChangeRequest --> TaskStatus : uses +Task --> Project : uses +Task --> Sprint : uses +Task --> TaskStatus : uses +Task --> TaskPriority : uses +Task --> User : uses +TaskActivity --> Task : uses +TaskActivity --> User : uses +TaskActivity --> ActivityType : uses +TaskActivityResponse --> UserSummary : uses +TaskActivityResponse --> ActivityType : uses +TaskRequest --> TaskStatus : uses +TaskRequest --> TaskPriority : uses +TaskResponse --> SprintSummary : uses +TaskResponse --> TaskStatus : uses +TaskResponse --> TaskPriority : uses +TaskResponse --> UserSummary : uses +UserDetailsServiceImpl --> UserRepository : depends on +@enduml diff --git a/docs/diagrams/nlu.puml b/docs/diagrams/nlu.puml new file mode 100644 index 000000000..464a9f338 --- /dev/null +++ b/docs/diagrams/nlu.puml @@ -0,0 +1,30 @@ +@startuml +title NLU +skinparam classAttributeIconSize 0 +hide empty members +left to right direction + +package "NLU" { +class NaturalLanguageRouter { + + anthropicApiKey : String + + restTemplate : RestTemplate + + objectMapper : ObjectMapper +} + +class NluErrorMessages { +} + +class NluResult { + + status : NluStatus + + command : String + + params : Map + + missing : List +} + +enum NluStatus { +} + +} + +NluResult --> NluStatus : uses +@enduml diff --git a/docs/diagrams/notification.puml b/docs/diagrams/notification.puml new file mode 100644 index 000000000..82c4c964e --- /dev/null +++ b/docs/diagrams/notification.puml @@ -0,0 +1,38 @@ +@startuml +title Notification +skinparam classAttributeIconSize 0 +hide empty members +left to right direction + +package "Notification" { +class NotificationLog { + + id : Long + + recipient : User + + eventType : String + + message : String + + deliveryStatus : String + + sentAt : LocalDateTime +} + +interface NotificationLogRepository { +} + +class NotificationScheduler { + + sprintRepository : SprintRepository + + taskRepository : TaskRepository + + notificationService : NotificationService +} + +class NotificationService { + + notificationLogRepository : NotificationLogRepository + + telegramClient : TelegramClient +} + +} + +NotificationLog --> User : depends on +NotificationScheduler --> SprintRepository : depends on +NotificationScheduler --> TaskRepository : depends on +NotificationScheduler --> NotificationService : uses +NotificationService --> NotificationLogRepository : uses +@enduml diff --git a/docs/diagrams/repositories.puml b/docs/diagrams/repositories.puml new file mode 100644 index 000000000..9157a28ef --- /dev/null +++ b/docs/diagrams/repositories.puml @@ -0,0 +1,34 @@ +@startuml +title Repositories +skinparam classAttributeIconSize 0 +hide empty members +left to right direction + +package "Repositories" { +interface AuditLogRepository { +} + +interface NotificationLogRepository { +} + +interface ProjectMemberRepository { +} + +interface ProjectRepository { +} + +interface SprintRepository { +} + +interface TaskActivityRepository { +} + +interface TaskRepository { +} + +interface UserRepository { +} + +} + +@enduml diff --git a/docs/diagrams/services.puml b/docs/diagrams/services.puml new file mode 100644 index 000000000..3d290d89e --- /dev/null +++ b/docs/diagrams/services.puml @@ -0,0 +1,139 @@ +@startuml +title Services +skinparam classAttributeIconSize 0 +hide empty members +left to right direction + +package "Services" { +class AuditLogService { + + auditLogRepository : AuditLogRepository + + objectMapper : ObjectMapper +} + +class AuthService { + + userRepository : UserRepository + + passwordEncoder : PasswordEncoder + + jwtUtil : JwtUtil + + userMapper : UserMapper +} + +class DashboardService { + + projectService : ProjectService + + sprintRepository : SprintRepository + + taskRepository : TaskRepository + + memberRepository : ProjectMemberRepository +} + +class NotificationScheduler { + + sprintRepository : SprintRepository + + taskRepository : TaskRepository + + notificationService : NotificationService +} + +class NotificationService { + + notificationLogRepository : NotificationLogRepository + + telegramClient : TelegramClient +} + +class ProjectMemberService { + + memberRepository : ProjectMemberRepository + + projectService : ProjectService + + userService : UserService + + auditLogService : AuditLogService + + memberMapper : ProjectMemberMapper +} + +class ProjectService { + + projectRepository : ProjectRepository + + memberRepository : ProjectMemberRepository + + sprintRepository : SprintRepository + + taskRepository : TaskRepository + + activityRepository : TaskActivityRepository + + userService : UserService + + auditLogService : AuditLogService + + projectMapper : ProjectMapper +} + +class SprintService { + + sprintRepository : SprintRepository + + taskRepository : TaskRepository + + projectService : ProjectService + + userService : UserService + + auditLogService : AuditLogService + + sprintMapper : SprintMapper +} + +class TaskActivityService { + + activityRepository : TaskActivityRepository + + taskService : TaskService + + projectService : ProjectService + + userService : UserService + + activityMapper : TaskActivityMapper +} + +class TaskService { + + taskRepository : TaskRepository + + activityRepository : TaskActivityRepository + + sprintRepository : SprintRepository + + projectService : ProjectService + + userService : UserService + + auditLogService : AuditLogService + + notificationService : NotificationService + + taskMapper : TaskMapper +} + +class UserService { + + userRepository : UserRepository + + userMapper : UserMapper + + passwordEncoder : PasswordEncoder +} + +} + +AuditLogService --> AuditLogRepository : depends on +AuthService --> UserRepository : depends on +AuthService --> JwtUtil : depends on +AuthService --> UserMapper : depends on +DashboardService --> ProjectService : uses +DashboardService --> SprintRepository : depends on +DashboardService --> TaskRepository : depends on +DashboardService --> ProjectMemberRepository : depends on +NotificationScheduler --> SprintRepository : depends on +NotificationScheduler --> TaskRepository : depends on +NotificationScheduler --> NotificationService : uses +NotificationService --> NotificationLogRepository : depends on +ProjectMemberService --> ProjectMemberRepository : depends on +ProjectMemberService --> ProjectService : uses +ProjectMemberService --> UserService : uses +ProjectMemberService --> AuditLogService : uses +ProjectMemberService --> ProjectMemberMapper : depends on +ProjectService --> ProjectRepository : depends on +ProjectService --> ProjectMemberRepository : depends on +ProjectService --> SprintRepository : depends on +ProjectService --> TaskRepository : depends on +ProjectService --> TaskActivityRepository : depends on +ProjectService --> UserService : uses +ProjectService --> AuditLogService : uses +ProjectService --> ProjectMapper : depends on +SprintService --> SprintRepository : depends on +SprintService --> TaskRepository : depends on +SprintService --> ProjectService : uses +SprintService --> UserService : uses +SprintService --> AuditLogService : uses +SprintService --> SprintMapper : depends on +TaskActivityService --> TaskActivityRepository : depends on +TaskActivityService --> TaskService : uses +TaskActivityService --> ProjectService : uses +TaskActivityService --> UserService : uses +TaskActivityService --> TaskActivityMapper : depends on +TaskService --> TaskRepository : depends on +TaskService --> TaskActivityRepository : depends on +TaskService --> SprintRepository : depends on +TaskService --> ProjectService : uses +TaskService --> UserService : uses +TaskService --> AuditLogService : uses +TaskService --> NotificationService : uses +TaskService --> TaskMapper : depends on +UserService --> UserRepository : depends on +UserService --> UserMapper : depends on +@enduml