Spaces:
Build error
Build error
Ajay Yadav commited on
Commit ·
b4ca43a
1
Parent(s): 377d3b3
Initial deployment of da-reporting-dev
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- Dockerfile +27 -0
- README.md +34 -6
- build.gradle.kts +23 -0
- src/main/docker/Dockerfile +23 -0
- src/main/docker/Dockerfile.alpine-jlink +43 -0
- src/main/docker/Dockerfile.layered +34 -0
- src/main/docker/Dockerfile.native +20 -0
- src/main/java/com/dalab/reporting/DaReportingApplication.java +17 -0
- src/main/java/com/dalab/reporting/client/CatalogServiceClient.java +66 -0
- src/main/java/com/dalab/reporting/client/PolicyEngineServiceClient.java +59 -0
- src/main/java/com/dalab/reporting/common/exception/BadRequestException.java +15 -0
- src/main/java/com/dalab/reporting/common/exception/ConflictException.java +15 -0
- src/main/java/com/dalab/reporting/common/exception/ResourceNotFoundException.java +15 -0
- src/main/java/com/dalab/reporting/config/AsyncConfig.java +21 -0
- src/main/java/com/dalab/reporting/config/DashboardConfig.java +20 -0
- src/main/java/com/dalab/reporting/config/KafkaConsumerConfig.java +58 -0
- src/main/java/com/dalab/reporting/config/OpenAPIConfiguration.java +40 -0
- src/main/java/com/dalab/reporting/config/SecurityConfiguration.java +45 -0
- src/main/java/com/dalab/reporting/controller/DashboardController.java +159 -0
- src/main/java/com/dalab/reporting/controller/NotificationController.java +108 -0
- src/main/java/com/dalab/reporting/controller/ReportController.java +121 -0
- src/main/java/com/dalab/reporting/dto/ActionItemsDTO.java +173 -0
- src/main/java/com/dalab/reporting/dto/CostSavingsDTO.java +158 -0
- src/main/java/com/dalab/reporting/dto/GeneratedReportOutputDTO.java +26 -0
- src/main/java/com/dalab/reporting/dto/GovernanceScoreDTO.java +143 -0
- src/main/java/com/dalab/reporting/dto/NotificationPreferenceDTO.java +36 -0
- src/main/java/com/dalab/reporting/dto/NotificationRequestDTO.java +37 -0
- src/main/java/com/dalab/reporting/dto/ReportDefinitionDTO.java +36 -0
- src/main/java/com/dalab/reporting/dto/ReportGenerationRequestDTO.java +21 -0
- src/main/java/com/dalab/reporting/event/PolicyActionEventDTO.java +25 -0
- src/main/java/com/dalab/reporting/kafka/PolicyActionConsumer.java +144 -0
- src/main/java/com/dalab/reporting/mapper/NotificationPreferenceMapper.java +20 -0
- src/main/java/com/dalab/reporting/mapper/ReportMapper.java +31 -0
- src/main/java/com/dalab/reporting/model/GeneratedReport.java +146 -0
- src/main/java/com/dalab/reporting/model/NotificationChannel.java +8 -0
- src/main/java/com/dalab/reporting/model/ReportDefinition.java +135 -0
- src/main/java/com/dalab/reporting/model/ReportStatus.java +9 -0
- src/main/java/com/dalab/reporting/model/UserNotificationPreference.java +129 -0
- src/main/java/com/dalab/reporting/repository/GeneratedReportRepository.java +20 -0
- src/main/java/com/dalab/reporting/repository/ReportDefinitionRepository.java +15 -0
- src/main/java/com/dalab/reporting/repository/UserNotificationPreferenceRepository.java +20 -0
- src/main/java/com/dalab/reporting/service/DashboardService.java +410 -0
- src/main/java/com/dalab/reporting/service/IDashboardService.java +53 -0
- src/main/java/com/dalab/reporting/service/INotificationService.java +28 -0
- src/main/java/com/dalab/reporting/service/IReportService.java +33 -0
- src/main/java/com/dalab/reporting/service/impl/NotificationService.java +207 -0
- src/main/java/com/dalab/reporting/service/impl/ReportService.java +230 -0
- src/main/java/com/dalab/reporting/service/notification/IEmailNotificationProvider.java +5 -0
- src/main/java/com/dalab/reporting/service/notification/INotificationProvider.java +11 -0
- src/main/java/com/dalab/reporting/service/notification/ISlackNotificationProvider.java +6 -0
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM openjdk:21-jdk-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install required packages
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
curl \
|
| 8 |
+
wget \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
# Copy application files
|
| 12 |
+
COPY . .
|
| 13 |
+
|
| 14 |
+
# Build application (if build.gradle.kts exists)
|
| 15 |
+
RUN if [ -f "build.gradle.kts" ]; then \
|
| 16 |
+
./gradlew build -x test; \
|
| 17 |
+
fi
|
| 18 |
+
|
| 19 |
+
# Expose port
|
| 20 |
+
EXPOSE 8080
|
| 21 |
+
|
| 22 |
+
# Health check
|
| 23 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
| 24 |
+
CMD curl -f http://localhost:8080/actuator/health || exit 1
|
| 25 |
+
|
| 26 |
+
# Run application
|
| 27 |
+
CMD ["java", "-jar", "build/libs/da-reporting.jar"]
|
README.md
CHANGED
|
@@ -1,10 +1,38 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: da-reporting (dev)
|
| 3 |
+
emoji: 🔧
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 8080
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# da-reporting - dev Environment
|
| 11 |
+
|
| 12 |
+
This is the da-reporting microservice deployed in the dev environment.
|
| 13 |
+
|
| 14 |
+
## Features
|
| 15 |
+
|
| 16 |
+
- RESTful API endpoints
|
| 17 |
+
- Health monitoring via Actuator
|
| 18 |
+
- JWT authentication integration
|
| 19 |
+
- PostgreSQL database connectivity
|
| 20 |
+
|
| 21 |
+
## API Documentation
|
| 22 |
+
|
| 23 |
+
Once deployed, API documentation will be available at:
|
| 24 |
+
- Swagger UI: https://huggingface.co/spaces/dalabsai/da-reporting-dev/swagger-ui.html
|
| 25 |
+
- Health Check: https://huggingface.co/spaces/dalabsai/da-reporting-dev/actuator/health
|
| 26 |
+
|
| 27 |
+
## Environment
|
| 28 |
+
|
| 29 |
+
- **Environment**: dev
|
| 30 |
+
- **Port**: 8080
|
| 31 |
+
- **Java Version**: 21
|
| 32 |
+
- **Framework**: Spring Boot
|
| 33 |
+
|
| 34 |
+
## Deployment
|
| 35 |
+
|
| 36 |
+
This service is automatically deployed via the DALab CI/CD pipeline.
|
| 37 |
+
|
| 38 |
+
Last updated: 2025-06-16 23:39:54
|
build.gradle.kts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// da-reporting inherits common configuration from parent build.gradle.kts
|
| 2 |
+
// This build file adds reporting-specific dependencies
|
| 3 |
+
|
| 4 |
+
dependencies {
|
| 5 |
+
// DA-Protos common entities and utilities
|
| 6 |
+
implementation(project(":da-protos"))
|
| 7 |
+
|
| 8 |
+
// Email and notification dependencies
|
| 9 |
+
implementation("org.springframework.boot:spring-boot-starter-mail")
|
| 10 |
+
implementation("com.slack.api:slack-api-client:1.38.0")
|
| 11 |
+
|
| 12 |
+
// Cross-service communication for dashboard aggregation
|
| 13 |
+
implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.1.1")
|
| 14 |
+
implementation("org.springframework.boot:spring-boot-starter-cache")
|
| 15 |
+
|
| 16 |
+
// OpenAPI documentation for dashboard endpoints
|
| 17 |
+
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0")
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// Configure main application class
|
| 21 |
+
configure<org.springframework.boot.gradle.dsl.SpringBootExtension> {
|
| 22 |
+
mainClass.set("com.dalab.reporting.DaReportingApplication")
|
| 23 |
+
}
|
src/main/docker/Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Ultra-lean container using Google Distroless
|
| 2 |
+
# Expected final size: ~120-180MB (minimal base + JRE + JAR only)
|
| 3 |
+
|
| 4 |
+
FROM gcr.io/distroless/java21-debian12:nonroot
|
| 5 |
+
|
| 6 |
+
# Set working directory
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
# Copy JAR file
|
| 10 |
+
COPY build/libs/da-reporting.jar app.jar
|
| 11 |
+
|
| 12 |
+
# Expose standard Spring Boot port
|
| 13 |
+
EXPOSE 8080
|
| 14 |
+
|
| 15 |
+
# Run application (distroless has no shell, so use exec form)
|
| 16 |
+
ENTRYPOINT ["java", \
|
| 17 |
+
"-XX:+UseContainerSupport", \
|
| 18 |
+
"-XX:MaxRAMPercentage=75.0", \
|
| 19 |
+
"-XX:+UseG1GC", \
|
| 20 |
+
"-XX:+UseStringDeduplication", \
|
| 21 |
+
"-Djava.security.egd=file:/dev/./urandom", \
|
| 22 |
+
"-Dspring.backgroundpreinitializer.ignore=true", \
|
| 23 |
+
"-jar", "app.jar"]
|
src/main/docker/Dockerfile.alpine-jlink
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Ultra-minimal Alpine + Custom JRE
|
| 2 |
+
# Expected size: ~120-160MB
|
| 3 |
+
|
| 4 |
+
# Stage 1: Create custom JRE with only needed modules
|
| 5 |
+
FROM eclipse-temurin:21-jdk-alpine as jre-builder
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
# Analyze JAR to find required modules
|
| 9 |
+
COPY build/libs/*.jar app.jar
|
| 10 |
+
RUN jdeps --ignore-missing-deps --print-module-deps app.jar > modules.txt
|
| 11 |
+
|
| 12 |
+
# Create minimal JRE with only required modules
|
| 13 |
+
RUN jlink \
|
| 14 |
+
--add-modules $(cat modules.txt),java.logging,java.xml,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument \
|
| 15 |
+
--strip-debug \
|
| 16 |
+
--no-man-pages \
|
| 17 |
+
--no-header-files \
|
| 18 |
+
--compress=2 \
|
| 19 |
+
--output /custom-jre
|
| 20 |
+
|
| 21 |
+
# Stage 2: Production image
|
| 22 |
+
FROM alpine:3.19
|
| 23 |
+
RUN apk add --no-cache tzdata && \
|
| 24 |
+
addgroup -g 1001 -S appgroup && \
|
| 25 |
+
adduser -u 1001 -S appuser -G appgroup
|
| 26 |
+
|
| 27 |
+
# Copy custom JRE
|
| 28 |
+
COPY --from=jre-builder /custom-jre /opt/java
|
| 29 |
+
ENV JAVA_HOME=/opt/java
|
| 30 |
+
ENV PATH="$JAVA_HOME/bin:$PATH"
|
| 31 |
+
|
| 32 |
+
WORKDIR /app
|
| 33 |
+
COPY build/libs/*.jar app.jar
|
| 34 |
+
RUN chown appuser:appgroup app.jar
|
| 35 |
+
|
| 36 |
+
USER appuser
|
| 37 |
+
EXPOSE 8080
|
| 38 |
+
|
| 39 |
+
ENTRYPOINT ["java", \
|
| 40 |
+
"-XX:+UseContainerSupport", \
|
| 41 |
+
"-XX:MaxRAMPercentage=70.0", \
|
| 42 |
+
"-XX:+UseG1GC", \
|
| 43 |
+
"-jar", "app.jar"]
|
src/main/docker/Dockerfile.layered
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Ultra-optimized layered build using Distroless
|
| 2 |
+
# Expected size: ~180-220MB with better caching
|
| 3 |
+
|
| 4 |
+
FROM gcr.io/distroless/java21-debian12:nonroot as base
|
| 5 |
+
|
| 6 |
+
# Stage 1: Extract JAR layers for optimal caching
|
| 7 |
+
FROM eclipse-temurin:21-jdk-alpine as extractor
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
COPY build/libs/*.jar app.jar
|
| 10 |
+
RUN java -Djarmode=layertools -jar app.jar extract
|
| 11 |
+
|
| 12 |
+
# Stage 2: Production image with extracted layers
|
| 13 |
+
FROM base
|
| 14 |
+
WORKDIR /app
|
| 15 |
+
|
| 16 |
+
# Copy layers in dependency order (best caching)
|
| 17 |
+
COPY --from=extractor /app/dependencies/ ./
|
| 18 |
+
COPY --from=extractor /app/spring-boot-loader/ ./
|
| 19 |
+
COPY --from=extractor /app/snapshot-dependencies/ ./
|
| 20 |
+
COPY --from=extractor /app/application/ ./
|
| 21 |
+
|
| 22 |
+
EXPOSE 8080
|
| 23 |
+
|
| 24 |
+
# Optimized JVM settings for micro-containers
|
| 25 |
+
ENTRYPOINT ["java", \
|
| 26 |
+
"-XX:+UseContainerSupport", \
|
| 27 |
+
"-XX:MaxRAMPercentage=70.0", \
|
| 28 |
+
"-XX:+UseG1GC", \
|
| 29 |
+
"-XX:+UseStringDeduplication", \
|
| 30 |
+
"-XX:+CompactStrings", \
|
| 31 |
+
"-Xshare:on", \
|
| 32 |
+
"-Djava.security.egd=file:/dev/./urandom", \
|
| 33 |
+
"-Dspring.backgroundpreinitializer.ignore=true", \
|
| 34 |
+
"org.springframework.boot.loader.JarLauncher"]
|
src/main/docker/Dockerfile.native
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GraalVM Native Image - Ultra-fast startup, tiny size
|
| 2 |
+
# Expected size: ~50-80MB, startup <100ms
|
| 3 |
+
# Note: Requires native compilation support in Spring Boot
|
| 4 |
+
|
| 5 |
+
# Stage 1: Native compilation
|
| 6 |
+
FROM ghcr.io/graalvm/graalvm-ce:ol9-java21 as native-builder
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
# Install native-image
|
| 10 |
+
RUN gu install native-image
|
| 11 |
+
|
| 12 |
+
# Copy source and build native executable
|
| 13 |
+
COPY . .
|
| 14 |
+
RUN ./gradlew nativeCompile
|
| 15 |
+
|
| 16 |
+
# Stage 2: Minimal runtime
|
| 17 |
+
FROM scratch
|
| 18 |
+
COPY --from=native-builder /app/build/native/nativeCompile/app /app
|
| 19 |
+
EXPOSE 8080
|
| 20 |
+
ENTRYPOINT ["/app"]
|
src/main/java/com/dalab/reporting/DaReportingApplication.java
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting;
|
| 2 |
+
|
| 3 |
+
import org.springframework.boot.SpringApplication;
|
| 4 |
+
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
| 5 |
+
import org.springframework.scheduling.annotation.EnableAsync; // For async email/slack sending
|
| 6 |
+
import org.springframework.scheduling.annotation.EnableScheduling; // If scheduled report generation is needed
|
| 7 |
+
|
| 8 |
+
@SpringBootApplication
|
| 9 |
+
@EnableScheduling // Optional: if reports are generated on a schedule
|
| 10 |
+
@EnableAsync // Optional: for non-blocking notification sending
|
| 11 |
+
public class DaReportingApplication {
|
| 12 |
+
|
| 13 |
+
public static void main(String[] args) {
|
| 14 |
+
SpringApplication.run(DaReportingApplication.class, args);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
}
|
src/main/java/com/dalab/reporting/client/CatalogServiceClient.java
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.client;
|
| 2 |
+
|
| 3 |
+
import java.util.Map;
|
| 4 |
+
|
| 5 |
+
import org.springframework.cloud.openfeign.FeignClient;
|
| 6 |
+
import org.springframework.web.bind.annotation.GetMapping;
|
| 7 |
+
import org.springframework.web.bind.annotation.RequestParam;
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* Feign client for da-catalog service
|
| 11 |
+
* Enables cross-service communication for dashboard aggregation
|
| 12 |
+
*
|
| 13 |
+
* @author DALab Development Team
|
| 14 |
+
* @since 2025-01-02
|
| 15 |
+
*/
|
| 16 |
+
@FeignClient(name = "da-catalog", url = "${dalab.services.da-catalog.url:http://localhost:8081}")
|
| 17 |
+
public interface CatalogServiceClient {
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* Get total asset count for governance calculations
|
| 21 |
+
*/
|
| 22 |
+
@GetMapping("/api/v1/catalog/assets/count")
|
| 23 |
+
Long getTotalAssetCount();
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* Get classified assets count for data classification metrics
|
| 27 |
+
*/
|
| 28 |
+
@GetMapping("/api/v1/catalog/assets/count/classified")
|
| 29 |
+
Long getClassifiedAssetsCount();
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* Get assets with metadata count for metadata completeness metrics
|
| 33 |
+
*/
|
| 34 |
+
@GetMapping("/api/v1/catalog/assets/count/with-metadata")
|
| 35 |
+
Long getAssetsWithMetadataCount();
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* Get assets with business context count for governance scoring
|
| 39 |
+
*/
|
| 40 |
+
@GetMapping("/api/v1/catalog/assets/count/with-business-context")
|
| 41 |
+
Long getAssetsWithBusinessContextCount();
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* Get assets with lineage count for lineage coverage metrics
|
| 45 |
+
*/
|
| 46 |
+
@GetMapping("/api/v1/catalog/assets/count/with-lineage")
|
| 47 |
+
Long getAssetsWithLineageCount();
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Get total lineage connections count
|
| 51 |
+
*/
|
| 52 |
+
@GetMapping("/api/v1/catalog/assets/lineage/connections/count")
|
| 53 |
+
Long getTotalLineageConnectionsCount();
|
| 54 |
+
|
| 55 |
+
/**
|
| 56 |
+
* Get governance issues from catalog service
|
| 57 |
+
*/
|
| 58 |
+
@GetMapping("/api/v1/catalog/assets/governance-issues")
|
| 59 |
+
Map<String, Object> getGovernanceIssues(@RequestParam(required = false) String severity);
|
| 60 |
+
|
| 61 |
+
/**
|
| 62 |
+
* Get asset classification breakdown for scoring
|
| 63 |
+
*/
|
| 64 |
+
@GetMapping("/api/v1/catalog/assets/classification-breakdown")
|
| 65 |
+
Map<String, Long> getClassificationBreakdown();
|
| 66 |
+
}
|
src/main/java/com/dalab/reporting/client/PolicyEngineServiceClient.java
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.client;
|
| 2 |
+
|
| 3 |
+
import org.springframework.cloud.openfeign.FeignClient;
|
| 4 |
+
import org.springframework.web.bind.annotation.GetMapping;
|
| 5 |
+
import org.springframework.web.bind.annotation.RequestParam;
|
| 6 |
+
|
| 7 |
+
import java.util.List;
|
| 8 |
+
import java.util.Map;
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* Feign client for da-policyengine service
|
| 12 |
+
* Enables cross-service communication for policy compliance data
|
| 13 |
+
*
|
| 14 |
+
* @author DALab Development Team
|
| 15 |
+
* @since 2025-01-02
|
| 16 |
+
*/
|
| 17 |
+
@FeignClient(name = "da-policyengine", url = "${dalab.services.da-policyengine.url:http://localhost:8082}")
|
| 18 |
+
public interface PolicyEngineServiceClient {
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Get active policies count for governance metrics
|
| 22 |
+
*/
|
| 23 |
+
@GetMapping("/api/v1/policyengine/policies/count/active")
|
| 24 |
+
Long getActivePoliciesCount();
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Get policy violations count for compliance scoring
|
| 28 |
+
*/
|
| 29 |
+
@GetMapping("/api/v1/policyengine/policies/violations/count")
|
| 30 |
+
Long getPolicyViolationsCount(@RequestParam(required = false) String severity);
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* Get policy compliance rate
|
| 34 |
+
*/
|
| 35 |
+
@GetMapping("/api/v1/policyengine/policies/compliance-rate")
|
| 36 |
+
Double getPolicyComplianceRate();
|
| 37 |
+
|
| 38 |
+
/**
|
| 39 |
+
* Get policy compliance breakdown by category
|
| 40 |
+
*/
|
| 41 |
+
@GetMapping("/api/v1/policyengine/policies/compliance-breakdown")
|
| 42 |
+
Map<String, Object> getPolicyComplianceBreakdown();
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* Get policy action items for aggregation
|
| 46 |
+
*/
|
| 47 |
+
@GetMapping("/api/v1/policyengine/policies/action-items")
|
| 48 |
+
List<Map<String, Object>> getPolicyActionItems(
|
| 49 |
+
@RequestParam(required = false) String severity,
|
| 50 |
+
@RequestParam(required = false) String status,
|
| 51 |
+
@RequestParam(defaultValue = "50") Integer limit);
|
| 52 |
+
|
| 53 |
+
/**
|
| 54 |
+
* Get historical policy compliance data
|
| 55 |
+
*/
|
| 56 |
+
@GetMapping("/api/v1/policyengine/policies/compliance-history")
|
| 57 |
+
List<Map<String, Object>> getPolicyComplianceHistory(
|
| 58 |
+
@RequestParam(defaultValue = "30") Integer days);
|
| 59 |
+
}
|
src/main/java/com/dalab/reporting/common/exception/BadRequestException.java
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.common.exception;
|
| 2 |
+
|
| 3 |
+
import org.springframework.http.HttpStatus;
|
| 4 |
+
import org.springframework.web.bind.annotation.ResponseStatus;
|
| 5 |
+
|
| 6 |
+
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
| 7 |
+
public class BadRequestException extends RuntimeException {
|
| 8 |
+
public BadRequestException(String message) {
|
| 9 |
+
super(message);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
public BadRequestException(String message, Throwable cause) {
|
| 13 |
+
super(message, cause);
|
| 14 |
+
}
|
| 15 |
+
}
|
src/main/java/com/dalab/reporting/common/exception/ConflictException.java
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.common.exception;
|
| 2 |
+
|
| 3 |
+
import org.springframework.http.HttpStatus;
|
| 4 |
+
import org.springframework.web.bind.annotation.ResponseStatus;
|
| 5 |
+
|
| 6 |
+
@ResponseStatus(HttpStatus.CONFLICT)
|
| 7 |
+
public class ConflictException extends RuntimeException {
|
| 8 |
+
public ConflictException(String message) {
|
| 9 |
+
super(message);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
public ConflictException(String message, Throwable cause) {
|
| 13 |
+
super(message, cause);
|
| 14 |
+
}
|
| 15 |
+
}
|
src/main/java/com/dalab/reporting/common/exception/ResourceNotFoundException.java
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.common.exception;
|
| 2 |
+
|
| 3 |
+
import org.springframework.http.HttpStatus;
|
| 4 |
+
import org.springframework.web.bind.annotation.ResponseStatus;
|
| 5 |
+
|
| 6 |
+
@ResponseStatus(HttpStatus.NOT_FOUND)
|
| 7 |
+
public class ResourceNotFoundException extends RuntimeException {
|
| 8 |
+
public ResourceNotFoundException(String message) {
|
| 9 |
+
super(message);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
|
| 13 |
+
super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
|
| 14 |
+
}
|
| 15 |
+
}
|
src/main/java/com/dalab/reporting/config/AsyncConfig.java
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.config;
|
| 2 |
+
|
| 3 |
+
import org.springframework.context.annotation.Bean;
|
| 4 |
+
import org.springframework.context.annotation.Configuration;
|
| 5 |
+
import org.springframework.core.task.TaskExecutor;
|
| 6 |
+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
| 7 |
+
|
| 8 |
+
@Configuration
|
| 9 |
+
public class AsyncConfig {
|
| 10 |
+
|
| 11 |
+
@Bean
|
| 12 |
+
public TaskExecutor taskExecutor() {
|
| 13 |
+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
| 14 |
+
executor.setCorePoolSize(5); // Adjust as needed
|
| 15 |
+
executor.setMaxPoolSize(10); // Adjust as needed
|
| 16 |
+
executor.setQueueCapacity(25); // Adjust as needed
|
| 17 |
+
executor.setThreadNamePrefix("ReportAsync-");
|
| 18 |
+
executor.initialize();
|
| 19 |
+
return executor;
|
| 20 |
+
}
|
| 21 |
+
}
|
src/main/java/com/dalab/reporting/config/DashboardConfig.java
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.config;
|
| 2 |
+
|
| 3 |
+
import org.springframework.cache.annotation.EnableCaching;
|
| 4 |
+
import org.springframework.cloud.openfeign.EnableFeignClients;
|
| 5 |
+
import org.springframework.context.annotation.Configuration;
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Configuration for dashboard aggregation infrastructure
|
| 9 |
+
* Includes basic caching and Feign clients for cross-service communication
|
| 10 |
+
*
|
| 11 |
+
* @author DALab Development Team
|
| 12 |
+
* @since 2025-01-02
|
| 13 |
+
*/
|
| 14 |
+
@Configuration
|
| 15 |
+
@EnableCaching
|
| 16 |
+
@EnableFeignClients(basePackages = "com.dalab.reporting.client")
|
| 17 |
+
public class DashboardConfig {
|
| 18 |
+
// Basic configuration for Feign clients and caching
|
| 19 |
+
// Redis and circuit breaker configuration can be added later
|
| 20 |
+
}
|
src/main/java/com/dalab/reporting/config/KafkaConsumerConfig.java
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.config;
|
| 2 |
+
|
| 3 |
+
import java.util.HashMap;
|
| 4 |
+
import java.util.Map;
|
| 5 |
+
|
| 6 |
+
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
| 7 |
+
import org.apache.kafka.common.serialization.StringDeserializer;
|
| 8 |
+
import org.springframework.beans.factory.annotation.Value;
|
| 9 |
+
import org.springframework.context.annotation.Bean;
|
| 10 |
+
import org.springframework.context.annotation.Configuration;
|
| 11 |
+
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
|
| 12 |
+
import org.springframework.kafka.core.ConsumerFactory;
|
| 13 |
+
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
|
| 14 |
+
import org.springframework.kafka.support.serializer.ErrorHandlingDeserializer;
|
| 15 |
+
import org.springframework.kafka.support.serializer.JsonDeserializer;
|
| 16 |
+
|
| 17 |
+
@Configuration
|
| 18 |
+
public class KafkaConsumerConfig {
|
| 19 |
+
|
| 20 |
+
@Value("${spring.kafka.bootstrap-servers}")
|
| 21 |
+
private String bootstrapServers;
|
| 22 |
+
|
| 23 |
+
@Value("${spring.kafka.consumer.group-id:reporting-group}") // Default group-id from properties
|
| 24 |
+
private String defaultGroupId;
|
| 25 |
+
|
| 26 |
+
@Value("${spring.kafka.consumer.properties.spring.json.trusted.packages:*}")
|
| 27 |
+
private String trustedPackages;
|
| 28 |
+
|
| 29 |
+
@Bean
|
| 30 |
+
public ConsumerFactory<String, Object> consumerFactory() {
|
| 31 |
+
Map<String, Object> props = new HashMap<>();
|
| 32 |
+
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
|
| 33 |
+
// group-id will be set per listener, but a default can be here if needed
|
| 34 |
+
// props.put(ConsumerConfig.GROUP_ID_CONFIG, defaultGroupId);
|
| 35 |
+
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
|
| 36 |
+
|
| 37 |
+
// Using ErrorHandlingDeserializer for both key and value
|
| 38 |
+
ErrorHandlingDeserializer<String> keyDe = new ErrorHandlingDeserializer<>(new StringDeserializer());
|
| 39 |
+
ErrorHandlingDeserializer<Object> valDe = new ErrorHandlingDeserializer<>(new JsonDeserializer<>(Object.class));
|
| 40 |
+
// Configure JsonDeserializer for trusted packages
|
| 41 |
+
props.put(JsonDeserializer.TRUSTED_PACKAGES, trustedPackages);
|
| 42 |
+
props.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, "false"); // If not using type headers
|
| 43 |
+
// If you expect specific types and don't rely on headers, you might need to configure value default type
|
| 44 |
+
// props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, "com.dalab.reporting.event.PolicyActionEventDTO");
|
| 45 |
+
|
| 46 |
+
return new DefaultKafkaConsumerFactory<>(props, keyDe, valDe);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
@Bean
|
| 50 |
+
public ConcurrentKafkaListenerContainerFactory<String, Object> kafkaListenerContainerFactory() {
|
| 51 |
+
ConcurrentKafkaListenerContainerFactory<String, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
|
| 52 |
+
factory.setConsumerFactory(consumerFactory());
|
| 53 |
+
// Add other configurations like concurrency, error handlers, filtering, etc. if needed
|
| 54 |
+
// factory.setConcurrency(3);
|
| 55 |
+
// factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.RECORD);
|
| 56 |
+
return factory;
|
| 57 |
+
}
|
| 58 |
+
}
|
src/main/java/com/dalab/reporting/config/OpenAPIConfiguration.java
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.config;
|
| 2 |
+
|
| 3 |
+
import org.springframework.beans.factory.annotation.Value;
|
| 4 |
+
import org.springframework.context.annotation.Bean;
|
| 5 |
+
import org.springframework.context.annotation.Configuration;
|
| 6 |
+
|
| 7 |
+
import io.swagger.v3.oas.models.Components;
|
| 8 |
+
import io.swagger.v3.oas.models.OpenAPI;
|
| 9 |
+
import io.swagger.v3.oas.models.info.Contact;
|
| 10 |
+
import io.swagger.v3.oas.models.info.Info;
|
| 11 |
+
import io.swagger.v3.oas.models.info.License;
|
| 12 |
+
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
| 13 |
+
import io.swagger.v3.oas.models.security.SecurityScheme;
|
| 14 |
+
|
| 15 |
+
@Configuration
|
| 16 |
+
public class OpenAPIConfiguration {
|
| 17 |
+
|
| 18 |
+
@Value("${spring.application.name:DALab Reporting Service}")
|
| 19 |
+
private String applicationName;
|
| 20 |
+
|
| 21 |
+
@Bean
|
| 22 |
+
public OpenAPI customOpenAPI() {
|
| 23 |
+
final String securitySchemeName = "bearerAuth";
|
| 24 |
+
|
| 25 |
+
return new OpenAPI()
|
| 26 |
+
.info(new Info().title(applicationName)
|
| 27 |
+
.description("API for DALab Reporting and Notification Service. " +
|
| 28 |
+
"Handles report generation, management, and user notifications via various channels.")
|
| 29 |
+
.version("v1.2.0") // Corresponds to API Design Document version
|
| 30 |
+
.contact(new Contact().name("DALab Support").email("support@dalab.com"))
|
| 31 |
+
.license(new License().name("Apache 2.0").url("http://springdoc.org")))
|
| 32 |
+
.addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
|
| 33 |
+
.components(new Components()
|
| 34 |
+
.addSecuritySchemes(securitySchemeName, new SecurityScheme()
|
| 35 |
+
.name(securitySchemeName)
|
| 36 |
+
.type(SecurityScheme.Type.HTTP)
|
| 37 |
+
.scheme("bearer")
|
| 38 |
+
.bearerFormat("JWT")));
|
| 39 |
+
}
|
| 40 |
+
}
|
src/main/java/com/dalab/reporting/config/SecurityConfiguration.java
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.config;
|
| 2 |
+
|
| 3 |
+
import org.springframework.beans.factory.annotation.Value;
|
| 4 |
+
import org.springframework.context.annotation.Bean;
|
| 5 |
+
import org.springframework.context.annotation.Configuration;
|
| 6 |
+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
| 7 |
+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
| 8 |
+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
| 9 |
+
import org.springframework.security.config.http.SessionCreationPolicy;
|
| 10 |
+
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
| 11 |
+
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
| 12 |
+
import org.springframework.security.web.SecurityFilterChain;
|
| 13 |
+
|
| 14 |
+
@Configuration
|
| 15 |
+
@EnableWebSecurity
|
| 16 |
+
@EnableMethodSecurity(prePostEnabled = true) // Enables @PreAuthorize, @PostAuthorize, etc.
|
| 17 |
+
public class SecurityConfiguration {
|
| 18 |
+
|
| 19 |
+
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
|
| 20 |
+
private String issuerUri;
|
| 21 |
+
|
| 22 |
+
@Bean
|
| 23 |
+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
| 24 |
+
http
|
| 25 |
+
.csrf(csrf -> csrf.disable()) // Consider enabling CSRF with proper handling if web UI is served
|
| 26 |
+
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
| 27 |
+
.authorizeHttpRequests(authz -> authz
|
| 28 |
+
.requestMatchers("/api/v1/reporting/actuator/**").permitAll() // Standard Spring Boot actuators
|
| 29 |
+
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() // OpenAPI docs
|
| 30 |
+
// .requestMatchers("/public/**").permitAll() // Example for public endpoints
|
| 31 |
+
.anyRequest().authenticated() // All other requests require authentication
|
| 32 |
+
)
|
| 33 |
+
.oauth2ResourceServer(oauth2 -> oauth2
|
| 34 |
+
.jwt(jwt -> jwt.decoder(jwtDecoder()))
|
| 35 |
+
);
|
| 36 |
+
return http.build();
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
@Bean
|
| 40 |
+
public JwtDecoder jwtDecoder() {
|
| 41 |
+
// Configure the decoder with the issuer URI from properties
|
| 42 |
+
// It will fetch the JWK set from the .well-known/jwks.json endpoint of the issuer
|
| 43 |
+
return NimbusJwtDecoder.withIssuerLocation(issuerUri).build();
|
| 44 |
+
}
|
| 45 |
+
}
|
src/main/java/com/dalab/reporting/controller/DashboardController.java
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.controller;
|
| 2 |
+
|
| 3 |
+
import org.slf4j.Logger;
|
| 4 |
+
import org.slf4j.LoggerFactory;
|
| 5 |
+
import org.springframework.cache.annotation.Cacheable;
|
| 6 |
+
import org.springframework.http.ResponseEntity;
|
| 7 |
+
import org.springframework.security.access.prepost.PreAuthorize;
|
| 8 |
+
import org.springframework.web.bind.annotation.GetMapping;
|
| 9 |
+
import org.springframework.web.bind.annotation.RequestMapping;
|
| 10 |
+
import org.springframework.web.bind.annotation.RequestParam;
|
| 11 |
+
import org.springframework.web.bind.annotation.RestController;
|
| 12 |
+
|
| 13 |
+
import com.dalab.common.security.SecurityUtils;
|
| 14 |
+
import com.dalab.reporting.dto.ActionItemsDTO;
|
| 15 |
+
import com.dalab.reporting.dto.CostSavingsDTO;
|
| 16 |
+
import com.dalab.reporting.dto.GovernanceScoreDTO;
|
| 17 |
+
import com.dalab.reporting.service.IDashboardService;
|
| 18 |
+
|
| 19 |
+
import io.swagger.v3.oas.annotations.Operation;
|
| 20 |
+
import io.swagger.v3.oas.annotations.Parameter;
|
| 21 |
+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
| 22 |
+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
| 23 |
+
import io.swagger.v3.oas.annotations.tags.Tag;
|
| 24 |
+
import lombok.RequiredArgsConstructor;
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* REST controller for DALab Dashboard Aggregation APIs
|
| 28 |
+
* Provides cross-service aggregation for role-based dashboards
|
| 29 |
+
*
|
| 30 |
+
* @author DALab Development Team
|
| 31 |
+
* @since 2025-01-02
|
| 32 |
+
*/
|
| 33 |
+
@RestController
|
| 34 |
+
@RequestMapping("/api/v1/reporting/dashboards")
|
| 35 |
+
@RequiredArgsConstructor
|
| 36 |
+
@Tag(name = "Dashboard", description = "Dashboard aggregation and analytics APIs")
|
| 37 |
+
public class DashboardController {
|
| 38 |
+
|
| 39 |
+
private static final Logger log = LoggerFactory.getLogger(DashboardController.class);
|
| 40 |
+
|
| 41 |
+
private final IDashboardService dashboardService;
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* Get governance score dashboard for CDO/VP and Director roles
|
| 45 |
+
* Aggregates data from da-catalog, da-autocompliance, and da-policyengine
|
| 46 |
+
*
|
| 47 |
+
* @param includeHistory whether to include 30-day historical trend data
|
| 48 |
+
* @param includeBenchmark whether to include industry benchmark comparison
|
| 49 |
+
* @return comprehensive governance score with breakdown and trends
|
| 50 |
+
*/
|
| 51 |
+
@GetMapping("/cdo/governance-score")
|
| 52 |
+
@PreAuthorize("hasAnyRole('CDO', 'VP_DATA', 'DIRECTOR_ENGINEERING', 'DIRECTOR_PRODUCT')")
|
| 53 |
+
@Cacheable(value = "governanceScore", key = "#includeHistory + '_' + #includeBenchmark")
|
| 54 |
+
@Operation(summary = "Get governance score dashboard",
|
| 55 |
+
description = "Provides comprehensive governance metrics with weighted scoring across policy compliance, data classification, metadata completeness, and lineage coverage")
|
| 56 |
+
@ApiResponses({
|
| 57 |
+
@ApiResponse(responseCode = "200", description = "Governance score retrieved successfully"),
|
| 58 |
+
@ApiResponse(responseCode = "403", description = "Access denied - insufficient privileges"),
|
| 59 |
+
@ApiResponse(responseCode = "500", description = "Internal server error during cross-service aggregation")
|
| 60 |
+
})
|
| 61 |
+
public ResponseEntity<GovernanceScoreDTO> getGovernanceScore(
|
| 62 |
+
@Parameter(description = "Include 30-day historical trend data")
|
| 63 |
+
@RequestParam(defaultValue = "true") Boolean includeHistory,
|
| 64 |
+
@Parameter(description = "Include industry benchmark comparison")
|
| 65 |
+
@RequestParam(defaultValue = "true") Boolean includeBenchmark) {
|
| 66 |
+
|
| 67 |
+
log.info("REST request to get governance score dashboard by user: {}", SecurityUtils.getAuthenticatedUserId());
|
| 68 |
+
GovernanceScoreDTO governanceScore = dashboardService.getGovernanceScore(includeHistory, includeBenchmark);
|
| 69 |
+
return ResponseEntity.ok(governanceScore);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
/**
|
| 73 |
+
* Get cost savings dashboard for CDO/VP and executive roles
|
| 74 |
+
* Aggregates data from da-autoarchival, da-autodelete, and cloud provider APIs
|
| 75 |
+
*
|
| 76 |
+
* @param includeProjections whether to include future savings projections
|
| 77 |
+
* @param timeframe analysis timeframe: "30_DAYS", "90_DAYS", "12_MONTHS", "ALL_TIME"
|
| 78 |
+
* @return comprehensive cost savings analysis with ROI calculation
|
| 79 |
+
*/
|
| 80 |
+
@GetMapping("/cdo/cost-savings")
|
| 81 |
+
@PreAuthorize("hasAnyRole('CDO', 'VP_DATA', 'DIRECTOR_ENGINEERING', 'ADMIN')")
|
| 82 |
+
@Cacheable(value = "costSavings", key = "#includeProjections + '_' + #timeframe")
|
| 83 |
+
@Operation(summary = "Get cost savings dashboard",
|
| 84 |
+
description = "Provides comprehensive cost analysis including archival savings, deletion reclamation, and ROI calculations across multiple cloud providers")
|
| 85 |
+
@ApiResponses({
|
| 86 |
+
@ApiResponse(responseCode = "200", description = "Cost savings data retrieved successfully"),
|
| 87 |
+
@ApiResponse(responseCode = "403", description = "Access denied - insufficient privileges"),
|
| 88 |
+
@ApiResponse(responseCode = "500", description = "Internal server error during cost calculation"),
|
| 89 |
+
@ApiResponse(responseCode = "503", description = "Cloud provider API temporarily unavailable")
|
| 90 |
+
})
|
| 91 |
+
public ResponseEntity<CostSavingsDTO> getCostSavings(
|
| 92 |
+
@Parameter(description = "Include future savings projections")
|
| 93 |
+
@RequestParam(defaultValue = "true") Boolean includeProjections,
|
| 94 |
+
@Parameter(description = "Analysis timeframe")
|
| 95 |
+
@RequestParam(defaultValue = "12_MONTHS") String timeframe) {
|
| 96 |
+
|
| 97 |
+
log.info("REST request to get cost savings dashboard for timeframe: {} by user: {}", timeframe, SecurityUtils.getAuthenticatedUserId());
|
| 98 |
+
CostSavingsDTO costSavings = dashboardService.getCostSavings(includeProjections, timeframe);
|
| 99 |
+
return ResponseEntity.ok(costSavings);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/**
|
| 103 |
+
* Get aggregated action items across all DALab services
|
| 104 |
+
* Aggregates data from da-autocompliance, da-policyengine, da-catalog, da-discovery
|
| 105 |
+
*
|
| 106 |
+
* @param assignedToUser filter to show only items assigned to current user
|
| 107 |
+
* @param severity filter by severity level: "CRITICAL", "HIGH", "MEDIUM", "LOW"
|
| 108 |
+
* @param status filter by status: "OPEN", "IN_PROGRESS", "PENDING_APPROVAL"
|
| 109 |
+
* @param limit maximum number of action items to return (default 50, max 200)
|
| 110 |
+
* @return comprehensive action items aggregation with SLA and business impact metrics
|
| 111 |
+
*/
|
| 112 |
+
@GetMapping("/cdo/action-items")
|
| 113 |
+
@PreAuthorize("hasAnyRole('CDO', 'VP_DATA', 'DIRECTOR_ENGINEERING', 'DIRECTOR_PRODUCT', 'DATA_ENGINEER', 'DATA_SCIENTIST')")
|
| 114 |
+
@Cacheable(value = "actionItems", key = "#assignedToUser + '_' + #severity + '_' + #status + '_' + #limit")
|
| 115 |
+
@Operation(summary = "Get aggregated action items dashboard",
|
| 116 |
+
description = "Provides comprehensive action items aggregation across all DALab services with SLA compliance metrics and business impact assessment")
|
| 117 |
+
@ApiResponses({
|
| 118 |
+
@ApiResponse(responseCode = "200", description = "Action items retrieved successfully"),
|
| 119 |
+
@ApiResponse(responseCode = "400", description = "Invalid filter parameters"),
|
| 120 |
+
@ApiResponse(responseCode = "403", description = "Access denied - insufficient privileges"),
|
| 121 |
+
@ApiResponse(responseCode = "500", description = "Internal server error during aggregation")
|
| 122 |
+
})
|
| 123 |
+
public ResponseEntity<ActionItemsDTO> getActionItems(
|
| 124 |
+
@Parameter(description = "Filter to show only items assigned to current user")
|
| 125 |
+
@RequestParam(defaultValue = "false") Boolean assignedToUser,
|
| 126 |
+
@Parameter(description = "Filter by severity level")
|
| 127 |
+
@RequestParam(required = false) String severity,
|
| 128 |
+
@Parameter(description = "Filter by status")
|
| 129 |
+
@RequestParam(required = false) String status,
|
| 130 |
+
@Parameter(description = "Maximum number of items to return")
|
| 131 |
+
@RequestParam(defaultValue = "50") Integer limit) {
|
| 132 |
+
|
| 133 |
+
log.info("REST request to get action items dashboard with filters - assignedToUser: {}, severity: {}, status: {}, limit: {} by user: {}",
|
| 134 |
+
assignedToUser, severity, status, limit, SecurityUtils.getAuthenticatedUserId());
|
| 135 |
+
|
| 136 |
+
// Validate limit parameter
|
| 137 |
+
if (limit > 200) {
|
| 138 |
+
limit = 200;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
ActionItemsDTO actionItems = dashboardService.getActionItems(assignedToUser, severity, status, limit);
|
| 142 |
+
return ResponseEntity.ok(actionItems);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/**
|
| 146 |
+
* Refresh dashboard cache for better performance
|
| 147 |
+
* Useful for scheduled cache warming or manual refresh
|
| 148 |
+
*/
|
| 149 |
+
@GetMapping("/cache/refresh")
|
| 150 |
+
@PreAuthorize("hasAnyRole('ADMIN', 'CDO')")
|
| 151 |
+
@Operation(summary = "Refresh dashboard cache",
|
| 152 |
+
description = "Manually triggers cache refresh for all dashboard endpoints to ensure fresh data")
|
| 153 |
+
@ApiResponse(responseCode = "200", description = "Cache refresh initiated successfully")
|
| 154 |
+
public ResponseEntity<String> refreshDashboardCache() {
|
| 155 |
+
log.info("REST request to refresh dashboard cache by user: {}", SecurityUtils.getAuthenticatedUserId());
|
| 156 |
+
dashboardService.refreshAllCaches();
|
| 157 |
+
return ResponseEntity.ok("Dashboard cache refresh initiated successfully");
|
| 158 |
+
}
|
| 159 |
+
}
|
src/main/java/com/dalab/reporting/controller/NotificationController.java
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.controller;
|
| 2 |
+
|
| 3 |
+
import java.util.List;
|
| 4 |
+
import java.util.UUID;
|
| 5 |
+
|
| 6 |
+
import org.slf4j.Logger;
|
| 7 |
+
import org.slf4j.LoggerFactory;
|
| 8 |
+
import org.springframework.data.domain.Page;
|
| 9 |
+
import org.springframework.data.domain.Pageable;
|
| 10 |
+
import org.springframework.http.HttpStatus;
|
| 11 |
+
import org.springframework.http.ResponseEntity;
|
| 12 |
+
import org.springframework.security.access.prepost.PreAuthorize;
|
| 13 |
+
// import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
| 14 |
+
// import org.springframework.security.oauth2.jwt.Jwt;
|
| 15 |
+
import org.springframework.web.bind.annotation.DeleteMapping;
|
| 16 |
+
import org.springframework.web.bind.annotation.GetMapping;
|
| 17 |
+
import org.springframework.web.bind.annotation.PathVariable;
|
| 18 |
+
import org.springframework.web.bind.annotation.PostMapping;
|
| 19 |
+
import org.springframework.web.bind.annotation.RequestBody;
|
| 20 |
+
import org.springframework.web.bind.annotation.RequestMapping;
|
| 21 |
+
import org.springframework.web.bind.annotation.ResponseStatus;
|
| 22 |
+
import org.springframework.web.bind.annotation.RestController;
|
| 23 |
+
|
| 24 |
+
import com.dalab.common.security.SecurityUtils;
|
| 25 |
+
import com.dalab.reporting.dto.NotificationPreferenceDTO;
|
| 26 |
+
import com.dalab.reporting.service.INotificationService;
|
| 27 |
+
|
| 28 |
+
import jakarta.validation.Valid;
|
| 29 |
+
import lombok.RequiredArgsConstructor;
|
| 30 |
+
|
| 31 |
+
@RestController
|
| 32 |
+
@RequestMapping("/api/v1/reporting/notifications")
|
| 33 |
+
@RequiredArgsConstructor
|
| 34 |
+
public class NotificationController {
|
| 35 |
+
|
| 36 |
+
private static final Logger log = LoggerFactory.getLogger(NotificationController.class);
|
| 37 |
+
|
| 38 |
+
private final INotificationService notificationService;
|
| 39 |
+
|
| 40 |
+
// TODO: Replace with actual authenticated user ID retrieval
|
| 41 |
+
private UUID getAuthenticatedUserId(/*@AuthenticationPrincipal Jwt jwt*/) {
|
| 42 |
+
// return UUID.fromString(jwt.getSubject());
|
| 43 |
+
return UUID.randomUUID(); // Placeholder
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
@PostMapping("/preferences")
|
| 47 |
+
@PreAuthorize("isAuthenticated()") // User manages their own preferences
|
| 48 |
+
public ResponseEntity<NotificationPreferenceDTO> saveNotificationPreference(@Valid @RequestBody NotificationPreferenceDTO preferenceDTO) {
|
| 49 |
+
// Ensure the userId in DTO matches the authenticated user, or is set by the backend
|
| 50 |
+
UUID authenticatedUserId = SecurityUtils.getAuthenticatedUserId();
|
| 51 |
+
if (preferenceDTO.getUserId() == null) {
|
| 52 |
+
preferenceDTO.setUserId(authenticatedUserId);
|
| 53 |
+
} else if (!preferenceDTO.getUserId().equals(authenticatedUserId)) {
|
| 54 |
+
// Non-admins should not be able to set preferences for other users.
|
| 55 |
+
// Admins might have a separate endpoint or logic.
|
| 56 |
+
// For now, let's assume this endpoint is for the authenticated user only.
|
| 57 |
+
throw new org.springframework.security.access.AccessDeniedException("Cannot set preferences for another user.");
|
| 58 |
+
}
|
| 59 |
+
return new ResponseEntity<>(notificationService.saveNotificationPreference(preferenceDTO), HttpStatus.OK);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
@GetMapping("/preferences/my")
|
| 63 |
+
@PreAuthorize("isAuthenticated()")
|
| 64 |
+
public ResponseEntity<List<NotificationPreferenceDTO>> getMyNotificationPreferences() {
|
| 65 |
+
UUID userId = SecurityUtils.getAuthenticatedUserId();
|
| 66 |
+
return ResponseEntity.ok(notificationService.getNotificationPreferencesByUserId(userId));
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
@GetMapping("/preferences/user/{userId}")
|
| 70 |
+
@PreAuthorize("hasRole('ADMIN')") // Admin can view others' preferences
|
| 71 |
+
public ResponseEntity<List<NotificationPreferenceDTO>> getUserNotificationPreferences(@PathVariable UUID userId) {
|
| 72 |
+
return ResponseEntity.ok(notificationService.getNotificationPreferencesByUserId(userId));
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
@GetMapping("/preferences/{preferenceId}")
|
| 76 |
+
@PreAuthorize("isAuthenticated()") // User can get their own, admin can get any
|
| 77 |
+
public ResponseEntity<NotificationPreferenceDTO> getNotificationPreferenceById(@PathVariable UUID preferenceId) {
|
| 78 |
+
// TODO: Add ownership check if not admin
|
| 79 |
+
return ResponseEntity.ok(notificationService.getNotificationPreferenceById(preferenceId));
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
@GetMapping("/preferences")
|
| 83 |
+
@PreAuthorize("hasRole('ADMIN')") // Admin only: get all preferences
|
| 84 |
+
public ResponseEntity<Page<NotificationPreferenceDTO>> getAllNotificationPreferences(Pageable pageable) {
|
| 85 |
+
return ResponseEntity.ok(notificationService.getAllNotificationPreferences(pageable));
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
@DeleteMapping("/preferences/{preferenceId}")
|
| 89 |
+
@PreAuthorize("isAuthenticated()") // User can delete their own, admin can delete any
|
| 90 |
+
@ResponseStatus(HttpStatus.NO_CONTENT)
|
| 91 |
+
public void deleteNotificationPreference(@PathVariable UUID preferenceId) {
|
| 92 |
+
// TODO: Add ownership check if not admin
|
| 93 |
+
notificationService.deleteNotificationPreference(preferenceId);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
@PostMapping("/test/{channelName}")
|
| 97 |
+
@PreAuthorize("isAuthenticated()")
|
| 98 |
+
public ResponseEntity<String> sendTestNotification(@PathVariable String channelName) {
|
| 99 |
+
UUID userId = SecurityUtils.getAuthenticatedUserId();
|
| 100 |
+
notificationService.sendTestNotification(userId, channelName);
|
| 101 |
+
return ResponseEntity.ok("Test notification sent to user " + userId + " via " + channelName);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// Note: A general purpose POST /notifications to send ad-hoc notifications is not exposed here.
|
| 105 |
+
// That would typically be an internal capability or a highly restricted admin API,
|
| 106 |
+
// as it could be misused for spam if `NotificationRequestDTO` is directly accepted.
|
| 107 |
+
// Notifications are primarily driven by events, report completions, or specific system actions.
|
| 108 |
+
}
|
src/main/java/com/dalab/reporting/controller/ReportController.java
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.controller;
|
| 2 |
+
|
| 3 |
+
import java.util.UUID;
|
| 4 |
+
|
| 5 |
+
import org.slf4j.Logger;
|
| 6 |
+
import org.slf4j.LoggerFactory;
|
| 7 |
+
import org.springframework.data.domain.Page;
|
| 8 |
+
import org.springframework.data.domain.Pageable;
|
| 9 |
+
import org.springframework.http.HttpStatus;
|
| 10 |
+
import org.springframework.http.ResponseEntity;
|
| 11 |
+
import org.springframework.security.access.prepost.PreAuthorize;
|
| 12 |
+
// import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
| 13 |
+
// import org.springframework.security.oauth2.jwt.Jwt;
|
| 14 |
+
import org.springframework.web.bind.annotation.DeleteMapping;
|
| 15 |
+
import org.springframework.web.bind.annotation.GetMapping;
|
| 16 |
+
import org.springframework.web.bind.annotation.PathVariable;
|
| 17 |
+
import org.springframework.web.bind.annotation.PostMapping;
|
| 18 |
+
import org.springframework.web.bind.annotation.PutMapping;
|
| 19 |
+
import org.springframework.web.bind.annotation.RequestBody;
|
| 20 |
+
import org.springframework.web.bind.annotation.RequestMapping;
|
| 21 |
+
import org.springframework.web.bind.annotation.ResponseStatus;
|
| 22 |
+
import org.springframework.web.bind.annotation.RestController;
|
| 23 |
+
|
| 24 |
+
import com.dalab.common.security.SecurityUtils;
|
| 25 |
+
import com.dalab.reporting.dto.GeneratedReportOutputDTO;
|
| 26 |
+
import com.dalab.reporting.dto.ReportDefinitionDTO;
|
| 27 |
+
import com.dalab.reporting.dto.ReportGenerationRequestDTO;
|
| 28 |
+
import com.dalab.reporting.service.IReportService;
|
| 29 |
+
|
| 30 |
+
import jakarta.validation.Valid;
|
| 31 |
+
import lombok.RequiredArgsConstructor;
|
| 32 |
+
|
| 33 |
+
@RestController
|
| 34 |
+
@RequestMapping("/api/v1/reporting")
|
| 35 |
+
@RequiredArgsConstructor
|
| 36 |
+
public class ReportController {
|
| 37 |
+
|
| 38 |
+
private static final Logger log = LoggerFactory.getLogger(ReportController.class);
|
| 39 |
+
|
| 40 |
+
private final IReportService reportService;
|
| 41 |
+
|
| 42 |
+
// --- Report Definitions ---
|
| 43 |
+
|
| 44 |
+
@PostMapping("/definitions")
|
| 45 |
+
@PreAuthorize("hasRole('ADMIN') or hasAuthority('SCOPE_manage:reports')")
|
| 46 |
+
public ResponseEntity<ReportDefinitionDTO> createReportDefinition(@Valid @RequestBody ReportDefinitionDTO reportDefinitionDTO) {
|
| 47 |
+
return new ResponseEntity<>(reportService.createReportDefinition(reportDefinitionDTO), HttpStatus.CREATED);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
@GetMapping("/definitions/{id}")
|
| 51 |
+
@PreAuthorize("isAuthenticated()")
|
| 52 |
+
public ResponseEntity<ReportDefinitionDTO> getReportDefinitionById(@PathVariable UUID id) {
|
| 53 |
+
return ResponseEntity.ok(reportService.getReportDefinitionById(id));
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
@GetMapping("/definitions/key/{reportKey}")
|
| 57 |
+
@PreAuthorize("isAuthenticated()")
|
| 58 |
+
public ResponseEntity<ReportDefinitionDTO> getReportDefinitionByKey(@PathVariable String reportKey) {
|
| 59 |
+
return ResponseEntity.ok(reportService.getReportDefinitionByKey(reportKey));
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
@GetMapping("/definitions")
|
| 63 |
+
@PreAuthorize("isAuthenticated()")
|
| 64 |
+
public ResponseEntity<Page<ReportDefinitionDTO>> getAllReportDefinitions(Pageable pageable) {
|
| 65 |
+
return ResponseEntity.ok(reportService.getAllReportDefinitions(pageable));
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
@PutMapping("/definitions/{id}")
|
| 69 |
+
@PreAuthorize("hasRole('ADMIN') or hasAuthority('SCOPE_manage:reports')")
|
| 70 |
+
public ResponseEntity<ReportDefinitionDTO> updateReportDefinition(@PathVariable UUID id, @Valid @RequestBody ReportDefinitionDTO reportDefinitionDTO) {
|
| 71 |
+
return ResponseEntity.ok(reportService.updateReportDefinition(id, reportDefinitionDTO));
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
@DeleteMapping("/definitions/{id}")
|
| 75 |
+
@PreAuthorize("hasRole('ADMIN') or hasAuthority('SCOPE_manage:reports')")
|
| 76 |
+
@ResponseStatus(HttpStatus.NO_CONTENT)
|
| 77 |
+
public void deleteReportDefinition(@PathVariable UUID id) {
|
| 78 |
+
reportService.deleteReportDefinition(id);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// --- Report Generation & Retrieval ---
|
| 82 |
+
|
| 83 |
+
@PostMapping("/key/{reportKey}/generate")
|
| 84 |
+
@PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_ANALYST')")
|
| 85 |
+
public ResponseEntity<GeneratedReportOutputDTO> generateReport(
|
| 86 |
+
@PathVariable String reportKey,
|
| 87 |
+
@RequestBody(required = false) ReportGenerationRequestDTO requestDTO) {
|
| 88 |
+
log.info("REST request to generate report: {}", reportKey);
|
| 89 |
+
UUID userId = SecurityUtils.getAuthenticatedUserId();
|
| 90 |
+
GeneratedReportOutputDTO generatedReport = reportService.generateReport(reportKey, requestDTO, userId);
|
| 91 |
+
return ResponseEntity.status(HttpStatus.ACCEPTED).body(generatedReport);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
@PostMapping("/id/{id}/generate")
|
| 95 |
+
@PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_ANALYST')")
|
| 96 |
+
public ResponseEntity<GeneratedReportOutputDTO> regenerateReport(@PathVariable UUID id) {
|
| 97 |
+
log.info("REST request to regenerate report: {}", id);
|
| 98 |
+
UUID userId = SecurityUtils.getAuthenticatedUserId();
|
| 99 |
+
GeneratedReportOutputDTO generatedReport = reportService.regenerateReport(id, userId);
|
| 100 |
+
return ResponseEntity.status(HttpStatus.ACCEPTED).body(generatedReport);
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
@GetMapping("/reports/jobs/{generatedReportId}")
|
| 104 |
+
@PreAuthorize("isAuthenticated()")
|
| 105 |
+
public ResponseEntity<GeneratedReportOutputDTO> getGeneratedReportStatus(@PathVariable UUID generatedReportId) {
|
| 106 |
+
// This DTO contains status, content (if ready), etc.
|
| 107 |
+
return ResponseEntity.ok(reportService.getGeneratedReportById(generatedReportId));
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
@GetMapping("/reports/key/{reportKey}")
|
| 111 |
+
@PreAuthorize("isAuthenticated()")
|
| 112 |
+
public ResponseEntity<Page<GeneratedReportOutputDTO>> getGeneratedReportsByReportKey(@PathVariable String reportKey, Pageable pageable) {
|
| 113 |
+
return ResponseEntity.ok(reportService.getGeneratedReportsByReportKey(reportKey, pageable));
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
@GetMapping("/reports")
|
| 117 |
+
@PreAuthorize("isAuthenticated()") // Might restrict further based on roles for seeing ALL reports
|
| 118 |
+
public ResponseEntity<Page<GeneratedReportOutputDTO>> getAllGeneratedReports(Pageable pageable) {
|
| 119 |
+
return ResponseEntity.ok(reportService.getAllGeneratedReports(pageable));
|
| 120 |
+
}
|
| 121 |
+
}
|
src/main/java/com/dalab/reporting/dto/ActionItemsDTO.java
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.dto;
|
| 2 |
+
|
| 3 |
+
import com.fasterxml.jackson.annotation.JsonFormat;
|
| 4 |
+
import lombok.AllArgsConstructor;
|
| 5 |
+
import lombok.Builder;
|
| 6 |
+
import lombok.Data;
|
| 7 |
+
import lombok.NoArgsConstructor;
|
| 8 |
+
|
| 9 |
+
import java.time.LocalDateTime;
|
| 10 |
+
import java.util.List;
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* DTO for Action Items Aggregation Dashboard response
|
| 14 |
+
* Provides comprehensive action items from all DALab services
|
| 15 |
+
*
|
| 16 |
+
* @author DALab Development Team
|
| 17 |
+
* @since 2025-01-02
|
| 18 |
+
*/
|
| 19 |
+
@Data
|
| 20 |
+
@Builder
|
| 21 |
+
@NoArgsConstructor
|
| 22 |
+
@AllArgsConstructor
|
| 23 |
+
public class ActionItemsDTO {
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* Total number of active action items
|
| 27 |
+
*/
|
| 28 |
+
private Integer totalActionItems;
|
| 29 |
+
|
| 30 |
+
/**
|
| 31 |
+
* Action items by priority/severity
|
| 32 |
+
*/
|
| 33 |
+
private ActionItemsSummary summary;
|
| 34 |
+
|
| 35 |
+
/**
|
| 36 |
+
* Detailed list of action items
|
| 37 |
+
*/
|
| 38 |
+
private List<ActionItem> actionItems;
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* SLA compliance metrics
|
| 42 |
+
*/
|
| 43 |
+
private SlaMetrics slaMetrics;
|
| 44 |
+
|
| 45 |
+
/**
|
| 46 |
+
* Business impact assessment
|
| 47 |
+
*/
|
| 48 |
+
private BusinessImpactSummary businessImpact;
|
| 49 |
+
|
| 50 |
+
/**
|
| 51 |
+
* Last aggregation timestamp
|
| 52 |
+
*/
|
| 53 |
+
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
| 54 |
+
private LocalDateTime lastAggregated;
|
| 55 |
+
|
| 56 |
+
/**
|
| 57 |
+
* Data sources included in aggregation
|
| 58 |
+
*/
|
| 59 |
+
private List<String> includedServices;
|
| 60 |
+
|
| 61 |
+
@Data
|
| 62 |
+
@Builder
|
| 63 |
+
@NoArgsConstructor
|
| 64 |
+
@AllArgsConstructor
|
| 65 |
+
public static class ActionItemsSummary {
|
| 66 |
+
private Integer criticalCount;
|
| 67 |
+
private Integer highCount;
|
| 68 |
+
private Integer mediumCount;
|
| 69 |
+
private Integer lowCount;
|
| 70 |
+
|
| 71 |
+
private Integer overdueCount;
|
| 72 |
+
private Integer dueTodayCount;
|
| 73 |
+
private Integer dueThisWeekCount;
|
| 74 |
+
|
| 75 |
+
private Integer assignedCount;
|
| 76 |
+
private Integer unassignedCount;
|
| 77 |
+
private Integer inProgressCount;
|
| 78 |
+
private Integer completedCount;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
@Data
|
| 82 |
+
@Builder
|
| 83 |
+
@NoArgsConstructor
|
| 84 |
+
@AllArgsConstructor
|
| 85 |
+
public static class ActionItem {
|
| 86 |
+
private String id;
|
| 87 |
+
private String title;
|
| 88 |
+
private String description;
|
| 89 |
+
private String category; // "COMPLIANCE_VIOLATION", "POLICY_BREACH", "METADATA_MISSING", "DISCOVERY_FAILED", etc.
|
| 90 |
+
private String severity; // "CRITICAL", "HIGH", "MEDIUM", "LOW"
|
| 91 |
+
private String status; // "OPEN", "IN_PROGRESS", "PENDING_APPROVAL", "COMPLETED", "DISMISSED"
|
| 92 |
+
|
| 93 |
+
private String sourceService; // "da-autocompliance", "da-policyengine", etc.
|
| 94 |
+
private String sourceEntityType; // "ASSET", "POLICY", "USER", "CONNECTION"
|
| 95 |
+
private String sourceEntityId;
|
| 96 |
+
private String sourceEntityName;
|
| 97 |
+
|
| 98 |
+
private String assignedTo; // user ID or team
|
| 99 |
+
private String assignedTeam; // "DATA_ENGINEERING", "COMPLIANCE", "SECURITY"
|
| 100 |
+
|
| 101 |
+
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
| 102 |
+
private LocalDateTime createdAt;
|
| 103 |
+
|
| 104 |
+
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
| 105 |
+
private LocalDateTime dueDate;
|
| 106 |
+
|
| 107 |
+
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
| 108 |
+
private LocalDateTime lastUpdated;
|
| 109 |
+
|
| 110 |
+
private String actionRequired;
|
| 111 |
+
private String businessJustification;
|
| 112 |
+
private String estimatedEffort; // "MINUTES", "HOURS", "DAYS", "WEEKS"
|
| 113 |
+
private Integer businessImpactScore; // 1-10
|
| 114 |
+
|
| 115 |
+
private List<String> tags;
|
| 116 |
+
private String detailsUrl; // link to specific service UI
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
@Data
|
| 120 |
+
@Builder
|
| 121 |
+
@NoArgsConstructor
|
| 122 |
+
@AllArgsConstructor
|
| 123 |
+
public static class SlaMetrics {
|
| 124 |
+
private Double averageResolutionTimeHours;
|
| 125 |
+
private Double slaComplianceRate; // percentage
|
| 126 |
+
private Integer breachedSlaCount;
|
| 127 |
+
private Integer nearBreachCount; // within 24 hours of SLA breach
|
| 128 |
+
|
| 129 |
+
private SlaByCategory complianceViolations;
|
| 130 |
+
private SlaByCategory policyBreaches;
|
| 131 |
+
private SlaByCategory metadataIssues;
|
| 132 |
+
private SlaByCategory discoveryIssues;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
@Data
|
| 136 |
+
@Builder
|
| 137 |
+
@NoArgsConstructor
|
| 138 |
+
@AllArgsConstructor
|
| 139 |
+
public static class SlaByCategory {
|
| 140 |
+
private Integer totalItems;
|
| 141 |
+
private Integer onTimeResolved;
|
| 142 |
+
private Integer breachedSla;
|
| 143 |
+
private Double averageResolutionHours;
|
| 144 |
+
private Double complianceRate;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
@Data
|
| 148 |
+
@Builder
|
| 149 |
+
@NoArgsConstructor
|
| 150 |
+
@AllArgsConstructor
|
| 151 |
+
public static class BusinessImpactSummary {
|
| 152 |
+
private Integer highImpactItems; // score 8-10
|
| 153 |
+
private Integer mediumImpactItems; // score 5-7
|
| 154 |
+
private Integer lowImpactItems; // score 1-4
|
| 155 |
+
|
| 156 |
+
private Double averageImpactScore;
|
| 157 |
+
private String highestImpactCategory;
|
| 158 |
+
|
| 159 |
+
private List<ImpactArea> impactAreas;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
@Data
|
| 163 |
+
@Builder
|
| 164 |
+
@NoArgsConstructor
|
| 165 |
+
@AllArgsConstructor
|
| 166 |
+
public static class ImpactArea {
|
| 167 |
+
private String areaName; // "DATA_PRIVACY", "REGULATORY_COMPLIANCE", "OPERATIONAL_EFFICIENCY", "COST_OPTIMIZATION"
|
| 168 |
+
private Integer affectedItems;
|
| 169 |
+
private Double averageImpactScore;
|
| 170 |
+
private String riskLevel; // "HIGH", "MEDIUM", "LOW"
|
| 171 |
+
private String description;
|
| 172 |
+
}
|
| 173 |
+
}
|
src/main/java/com/dalab/reporting/dto/CostSavingsDTO.java
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.dto;
|
| 2 |
+
|
| 3 |
+
import com.fasterxml.jackson.annotation.JsonFormat;
|
| 4 |
+
import lombok.AllArgsConstructor;
|
| 5 |
+
import lombok.Builder;
|
| 6 |
+
import lombok.Data;
|
| 7 |
+
import lombok.NoArgsConstructor;
|
| 8 |
+
|
| 9 |
+
import java.math.BigDecimal;
|
| 10 |
+
import java.time.LocalDateTime;
|
| 11 |
+
import java.util.List;
|
| 12 |
+
import java.util.Map;
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* DTO for Cost Savings Dashboard response
|
| 16 |
+
* Provides comprehensive cost analysis and savings opportunities
|
| 17 |
+
*
|
| 18 |
+
* @author DALab Development Team
|
| 19 |
+
* @since 2025-01-02
|
| 20 |
+
*/
|
| 21 |
+
@Data
|
| 22 |
+
@Builder
|
| 23 |
+
@NoArgsConstructor
|
| 24 |
+
@AllArgsConstructor
|
| 25 |
+
public class CostSavingsDTO {
|
| 26 |
+
|
| 27 |
+
/**
|
| 28 |
+
* Total cost savings achieved (USD)
|
| 29 |
+
*/
|
| 30 |
+
private BigDecimal totalSavings;
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* Monthly savings rate (USD per month)
|
| 34 |
+
*/
|
| 35 |
+
private BigDecimal monthlySavingsRate;
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* ROI percentage including platform costs
|
| 39 |
+
*/
|
| 40 |
+
private Double roiPercentage;
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Cost savings breakdown by category
|
| 44 |
+
*/
|
| 45 |
+
private CostSavingsBreakdown breakdown;
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* Savings by cloud provider
|
| 49 |
+
*/
|
| 50 |
+
private List<ProviderSavings> providerSavings;
|
| 51 |
+
|
| 52 |
+
/**
|
| 53 |
+
* Projected savings pipeline
|
| 54 |
+
*/
|
| 55 |
+
private List<SavingsOpportunity> savingsOpportunities;
|
| 56 |
+
|
| 57 |
+
/**
|
| 58 |
+
* Historical savings trend (last 12 months)
|
| 59 |
+
*/
|
| 60 |
+
private List<SavingsTrendPoint> historicalTrend;
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* Cost optimization recommendations
|
| 64 |
+
*/
|
| 65 |
+
private List<String> recommendations;
|
| 66 |
+
|
| 67 |
+
/**
|
| 68 |
+
* Last calculation timestamp
|
| 69 |
+
*/
|
| 70 |
+
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
| 71 |
+
private LocalDateTime lastCalculated;
|
| 72 |
+
|
| 73 |
+
/**
|
| 74 |
+
* Data accuracy indicator
|
| 75 |
+
*/
|
| 76 |
+
private String dataAccuracy; // "REAL_TIME", "DAILY", "ESTIMATED"
|
| 77 |
+
|
| 78 |
+
@Data
|
| 79 |
+
@Builder
|
| 80 |
+
@NoArgsConstructor
|
| 81 |
+
@AllArgsConstructor
|
| 82 |
+
public static class CostSavingsBreakdown {
|
| 83 |
+
|
| 84 |
+
/**
|
| 85 |
+
* Savings from archival operations
|
| 86 |
+
*/
|
| 87 |
+
private BigDecimal archivalSavings;
|
| 88 |
+
private Integer archivedAssetsCount;
|
| 89 |
+
private String archivalStorageUsed; // e.g., "2.5 TB"
|
| 90 |
+
|
| 91 |
+
/**
|
| 92 |
+
* Savings from deletion operations
|
| 93 |
+
*/
|
| 94 |
+
private BigDecimal deletionSavings;
|
| 95 |
+
private Integer deletedAssetsCount;
|
| 96 |
+
private String storageReclaimed; // e.g., "5.2 TB"
|
| 97 |
+
|
| 98 |
+
/**
|
| 99 |
+
* Savings from storage tier optimization
|
| 100 |
+
*/
|
| 101 |
+
private BigDecimal tierOptimizationSavings;
|
| 102 |
+
private Integer optimizedAssetsCount;
|
| 103 |
+
|
| 104 |
+
/**
|
| 105 |
+
* Compute cost reductions
|
| 106 |
+
*/
|
| 107 |
+
private BigDecimal computeSavings;
|
| 108 |
+
private Integer decommissionedResources;
|
| 109 |
+
|
| 110 |
+
/**
|
| 111 |
+
* Platform operational costs
|
| 112 |
+
*/
|
| 113 |
+
private BigDecimal platformCosts;
|
| 114 |
+
private BigDecimal netSavings; // totalSavings - platformCosts
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
@Data
|
| 118 |
+
@Builder
|
| 119 |
+
@NoArgsConstructor
|
| 120 |
+
@AllArgsConstructor
|
| 121 |
+
public static class ProviderSavings {
|
| 122 |
+
private String providerName; // "AWS", "GCP", "Azure", "OCI"
|
| 123 |
+
private BigDecimal savings;
|
| 124 |
+
private BigDecimal monthlyRate;
|
| 125 |
+
private String primarySavingsSource; // "ARCHIVAL", "DELETION", "OPTIMIZATION"
|
| 126 |
+
private Double contributionPercentage;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
@Data
|
| 130 |
+
@Builder
|
| 131 |
+
@NoArgsConstructor
|
| 132 |
+
@AllArgsConstructor
|
| 133 |
+
public static class SavingsOpportunity {
|
| 134 |
+
private String opportunityType; // "ARCHIVAL_CANDIDATE", "DELETION_CANDIDATE", "TIER_OPTIMIZATION"
|
| 135 |
+
private String description;
|
| 136 |
+
private BigDecimal potentialSavings;
|
| 137 |
+
private BigDecimal monthlyImpact;
|
| 138 |
+
private String timeframe; // "IMMEDIATE", "30_DAYS", "90_DAYS"
|
| 139 |
+
private String effortLevel; // "LOW", "MEDIUM", "HIGH"
|
| 140 |
+
private Integer affectedAssets;
|
| 141 |
+
private String providerName;
|
| 142 |
+
private String actionRequired;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
@Data
|
| 146 |
+
@Builder
|
| 147 |
+
@NoArgsConstructor
|
| 148 |
+
@AllArgsConstructor
|
| 149 |
+
public static class SavingsTrendPoint {
|
| 150 |
+
@JsonFormat(pattern = "yyyy-MM")
|
| 151 |
+
private LocalDateTime month;
|
| 152 |
+
private BigDecimal cumulativeSavings;
|
| 153 |
+
private BigDecimal monthlySavings;
|
| 154 |
+
private BigDecimal platformCosts;
|
| 155 |
+
private BigDecimal netSavings;
|
| 156 |
+
private Integer assetsProcessed;
|
| 157 |
+
}
|
| 158 |
+
}
|
src/main/java/com/dalab/reporting/dto/GeneratedReportOutputDTO.java
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.dto;
|
| 2 |
+
|
| 3 |
+
import java.time.Instant;
|
| 4 |
+
import java.util.Map;
|
| 5 |
+
import java.util.UUID;
|
| 6 |
+
|
| 7 |
+
import com.dalab.reporting.model.ReportStatus;
|
| 8 |
+
|
| 9 |
+
import lombok.Data;
|
| 10 |
+
import lombok.NoArgsConstructor;
|
| 11 |
+
|
| 12 |
+
@Data
|
| 13 |
+
@NoArgsConstructor
|
| 14 |
+
public class GeneratedReportOutputDTO {
|
| 15 |
+
private UUID id;
|
| 16 |
+
private String reportKey; // From associated ReportDefinition
|
| 17 |
+
private String reportDisplayName; // From associated ReportDefinition
|
| 18 |
+
private ReportStatus status;
|
| 19 |
+
private Map<String, Object> generationParameters;
|
| 20 |
+
private Object reportContent; // Can be String (URI, CSV) or Map/Object (JSON)
|
| 21 |
+
private String failureReason;
|
| 22 |
+
private Instant requestedAt;
|
| 23 |
+
private Instant startedAt;
|
| 24 |
+
private Instant completedAt;
|
| 25 |
+
private UUID requestedByUserId;
|
| 26 |
+
}
|
src/main/java/com/dalab/reporting/dto/GovernanceScoreDTO.java
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.dto;
|
| 2 |
+
|
| 3 |
+
import com.fasterxml.jackson.annotation.JsonFormat;
|
| 4 |
+
import lombok.AllArgsConstructor;
|
| 5 |
+
import lombok.Builder;
|
| 6 |
+
import lombok.Data;
|
| 7 |
+
import lombok.NoArgsConstructor;
|
| 8 |
+
|
| 9 |
+
import java.time.LocalDateTime;
|
| 10 |
+
import java.util.List;
|
| 11 |
+
import java.util.Map;
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* DTO for Governance Score Dashboard response
|
| 15 |
+
* Provides comprehensive governance metrics and breakdown
|
| 16 |
+
*
|
| 17 |
+
* @author DALab Development Team
|
| 18 |
+
* @since 2025-01-02
|
| 19 |
+
*/
|
| 20 |
+
@Data
|
| 21 |
+
@Builder
|
| 22 |
+
@NoArgsConstructor
|
| 23 |
+
@AllArgsConstructor
|
| 24 |
+
public class GovernanceScoreDTO {
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Overall governance score (0-100)
|
| 28 |
+
* Weighted calculation: Policy Compliance 30%, Data Classification 25%,
|
| 29 |
+
* Metadata Completeness 25%, Lineage Coverage 20%
|
| 30 |
+
*/
|
| 31 |
+
private Double overallScore;
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* Governance score breakdown by category
|
| 35 |
+
*/
|
| 36 |
+
private GovernanceBreakdown breakdown;
|
| 37 |
+
|
| 38 |
+
/**
|
| 39 |
+
* Historical trend data (last 30 days)
|
| 40 |
+
*/
|
| 41 |
+
private List<GovernanceTrendPoint> historicalTrend;
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* Top governance issues requiring attention
|
| 45 |
+
*/
|
| 46 |
+
private List<GovernanceIssue> topIssues;
|
| 47 |
+
|
| 48 |
+
/**
|
| 49 |
+
* Benchmark comparison against industry standards
|
| 50 |
+
*/
|
| 51 |
+
private BenchmarkComparison benchmark;
|
| 52 |
+
|
| 53 |
+
/**
|
| 54 |
+
* Score improvement recommendations
|
| 55 |
+
*/
|
| 56 |
+
private List<String> recommendations;
|
| 57 |
+
|
| 58 |
+
/**
|
| 59 |
+
* Last calculation timestamp
|
| 60 |
+
*/
|
| 61 |
+
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
| 62 |
+
private LocalDateTime lastCalculated;
|
| 63 |
+
|
| 64 |
+
/**
|
| 65 |
+
* Data freshness indicator (minutes since last update)
|
| 66 |
+
*/
|
| 67 |
+
private Integer dataFreshnessMinutes;
|
| 68 |
+
|
| 69 |
+
@Data
|
| 70 |
+
@Builder
|
| 71 |
+
@NoArgsConstructor
|
| 72 |
+
@AllArgsConstructor
|
| 73 |
+
public static class GovernanceBreakdown {
|
| 74 |
+
|
| 75 |
+
/**
|
| 76 |
+
* Policy compliance score (30% weight)
|
| 77 |
+
*/
|
| 78 |
+
private Double policyComplianceScore;
|
| 79 |
+
private Integer activePolicies;
|
| 80 |
+
private Integer violationsCount;
|
| 81 |
+
private Double complianceRate;
|
| 82 |
+
|
| 83 |
+
/**
|
| 84 |
+
* Data classification score (25% weight)
|
| 85 |
+
*/
|
| 86 |
+
private Double dataClassificationScore;
|
| 87 |
+
private Integer classifiedAssets;
|
| 88 |
+
private Integer totalAssets;
|
| 89 |
+
private Double classificationRate;
|
| 90 |
+
|
| 91 |
+
/**
|
| 92 |
+
* Metadata completeness score (25% weight)
|
| 93 |
+
*/
|
| 94 |
+
private Double metadataCompletenessScore;
|
| 95 |
+
private Integer assetsWithMetadata;
|
| 96 |
+
private Integer assetsWithBusinessContext;
|
| 97 |
+
private Double metadataCompletionRate;
|
| 98 |
+
|
| 99 |
+
/**
|
| 100 |
+
* Lineage coverage score (20% weight)
|
| 101 |
+
*/
|
| 102 |
+
private Double lineageCoverageScore;
|
| 103 |
+
private Integer assetsWithLineage;
|
| 104 |
+
private Integer totalLineageConnections;
|
| 105 |
+
private Double lineageCoverageRate;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
@Data
|
| 109 |
+
@Builder
|
| 110 |
+
@NoArgsConstructor
|
| 111 |
+
@AllArgsConstructor
|
| 112 |
+
public static class GovernanceTrendPoint {
|
| 113 |
+
@JsonFormat(pattern = "yyyy-MM-dd")
|
| 114 |
+
private LocalDateTime date;
|
| 115 |
+
private Double score;
|
| 116 |
+
private String period; // "daily", "weekly"
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
@Data
|
| 120 |
+
@Builder
|
| 121 |
+
@NoArgsConstructor
|
| 122 |
+
@AllArgsConstructor
|
| 123 |
+
public static class GovernanceIssue {
|
| 124 |
+
private String category; // "POLICY_VIOLATION", "MISSING_CLASSIFICATION", "INCOMPLETE_METADATA", "MISSING_LINEAGE"
|
| 125 |
+
private String description;
|
| 126 |
+
private String severity; // "HIGH", "MEDIUM", "LOW"
|
| 127 |
+
private Integer affectedAssets;
|
| 128 |
+
private String actionRequired;
|
| 129 |
+
private String assignedTeam;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
@Data
|
| 133 |
+
@Builder
|
| 134 |
+
@NoArgsConstructor
|
| 135 |
+
@AllArgsConstructor
|
| 136 |
+
public static class BenchmarkComparison {
|
| 137 |
+
private Double industryAverage;
|
| 138 |
+
private Double peerAverage;
|
| 139 |
+
private String ranking; // "EXCELLENT", "GOOD", "AVERAGE", "BELOW_AVERAGE", "POOR"
|
| 140 |
+
private Integer percentile; // 0-100
|
| 141 |
+
private String comparisonNote;
|
| 142 |
+
}
|
| 143 |
+
}
|
src/main/java/com/dalab/reporting/dto/NotificationPreferenceDTO.java
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.dto;
|
| 2 |
+
|
| 3 |
+
import java.time.Instant;
|
| 4 |
+
import java.util.Map;
|
| 5 |
+
import java.util.UUID;
|
| 6 |
+
|
| 7 |
+
import com.dalab.reporting.model.NotificationChannel;
|
| 8 |
+
|
| 9 |
+
import jakarta.validation.constraints.NotBlank;
|
| 10 |
+
import jakarta.validation.constraints.NotNull;
|
| 11 |
+
import jakarta.validation.constraints.Size;
|
| 12 |
+
import lombok.Data;
|
| 13 |
+
import lombok.NoArgsConstructor;
|
| 14 |
+
|
| 15 |
+
@Data
|
| 16 |
+
@NoArgsConstructor
|
| 17 |
+
public class NotificationPreferenceDTO {
|
| 18 |
+
private UUID id;
|
| 19 |
+
|
| 20 |
+
@NotNull(message = "User ID cannot be null")
|
| 21 |
+
private UUID userId;
|
| 22 |
+
|
| 23 |
+
@NotBlank(message = "Notification type key cannot be blank")
|
| 24 |
+
@Size(max = 100, message = "Notification type key must be less than 100 characters")
|
| 25 |
+
private String notificationTypeKey;
|
| 26 |
+
|
| 27 |
+
@NotNull(message = "Channel cannot be null")
|
| 28 |
+
private NotificationChannel channel;
|
| 29 |
+
|
| 30 |
+
private boolean enabled;
|
| 31 |
+
|
| 32 |
+
private Map<String, String> channelConfiguration;
|
| 33 |
+
|
| 34 |
+
private Instant createdAt;
|
| 35 |
+
private Instant updatedAt;
|
| 36 |
+
}
|
src/main/java/com/dalab/reporting/dto/NotificationRequestDTO.java
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.dto;
|
| 2 |
+
|
| 3 |
+
import com.dalab.reporting.model.NotificationChannel;
|
| 4 |
+
import jakarta.validation.constraints.NotBlank;
|
| 5 |
+
import jakarta.validation.constraints.NotEmpty;
|
| 6 |
+
import lombok.Builder;
|
| 7 |
+
import lombok.Data;
|
| 8 |
+
|
| 9 |
+
import java.util.List;
|
| 10 |
+
import java.util.Map;
|
| 11 |
+
import java.util.UUID;
|
| 12 |
+
|
| 13 |
+
@Data
|
| 14 |
+
@Builder
|
| 15 |
+
public class NotificationRequestDTO {
|
| 16 |
+
// Target specific user(s) by their IDs
|
| 17 |
+
private List<UUID> targetUserIds;
|
| 18 |
+
|
| 19 |
+
// Or target a specific notification type key (service will find subscribed users)
|
| 20 |
+
private String notificationTypeKey;
|
| 21 |
+
|
| 22 |
+
@NotEmpty(message = "At least one notification channel must be specified.")
|
| 23 |
+
private List<NotificationChannel> channels; // e.g., [EMAIL, SLACK]
|
| 24 |
+
|
| 25 |
+
@NotBlank(message = "Subject cannot be blank")
|
| 26 |
+
private String subject;
|
| 27 |
+
|
| 28 |
+
@NotBlank(message = "Body cannot be blank")
|
| 29 |
+
private String body;
|
| 30 |
+
|
| 31 |
+
// Optional: For rich content or structured data in notifications (e.g., Slack blocks, email template model)
|
| 32 |
+
private Map<String, Object> contentModel;
|
| 33 |
+
|
| 34 |
+
// Optional: Specific channel configurations to use for this notification, overriding user preferences or defaults.
|
| 35 |
+
// Key: "EMAIL" or "SLACK". Value: Map, e.g. for SLACK: {"targetChannelId": "#alerts"}
|
| 36 |
+
private Map<String, Map<String, String>> channelOverrides;
|
| 37 |
+
}
|
src/main/java/com/dalab/reporting/dto/ReportDefinitionDTO.java
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.dto;
|
| 2 |
+
|
| 3 |
+
import java.time.Instant;
|
| 4 |
+
import java.util.Map;
|
| 5 |
+
import java.util.UUID;
|
| 6 |
+
|
| 7 |
+
import jakarta.validation.constraints.NotBlank;
|
| 8 |
+
import jakarta.validation.constraints.Size;
|
| 9 |
+
import lombok.Data;
|
| 10 |
+
import lombok.NoArgsConstructor;
|
| 11 |
+
|
| 12 |
+
@Data
|
| 13 |
+
@NoArgsConstructor
|
| 14 |
+
public class ReportDefinitionDTO {
|
| 15 |
+
private UUID id;
|
| 16 |
+
|
| 17 |
+
@NotBlank(message = "Report key cannot be blank")
|
| 18 |
+
@Size(max = 100, message = "Report key must be less than 100 characters")
|
| 19 |
+
private String reportKey;
|
| 20 |
+
|
| 21 |
+
@NotBlank(message = "Display name cannot be blank")
|
| 22 |
+
@Size(max = 255, message = "Display name must be less than 255 characters")
|
| 23 |
+
private String displayName;
|
| 24 |
+
|
| 25 |
+
@Size(max = 1000, message = "Description must be less than 1000 characters")
|
| 26 |
+
private String description;
|
| 27 |
+
|
| 28 |
+
private Map<String, Object> defaultParameters;
|
| 29 |
+
|
| 30 |
+
@Size(max = 100, message = "Default schedule cron must be less than 100 characters")
|
| 31 |
+
private String defaultScheduleCron;
|
| 32 |
+
|
| 33 |
+
private boolean enabled;
|
| 34 |
+
private Instant createdAt;
|
| 35 |
+
private Instant updatedAt;
|
| 36 |
+
}
|
src/main/java/com/dalab/reporting/dto/ReportGenerationRequestDTO.java
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.dto;
|
| 2 |
+
|
| 3 |
+
import java.util.Map;
|
| 4 |
+
import java.util.UUID;
|
| 5 |
+
|
| 6 |
+
import lombok.Data;
|
| 7 |
+
import lombok.NoArgsConstructor;
|
| 8 |
+
|
| 9 |
+
@Data
|
| 10 |
+
@NoArgsConstructor
|
| 11 |
+
public class ReportGenerationRequestDTO {
|
| 12 |
+
// This DTO is primarily for triggering. For output, see GeneratedReportOutputDTO.
|
| 13 |
+
|
| 14 |
+
// Can be used to specify parameters overriding the ReportDefinition's defaults
|
| 15 |
+
private Map<String, Object> parameters;
|
| 16 |
+
|
| 17 |
+
// Optional: if a specific user ID should be associated with this request
|
| 18 |
+
// Otherwise, it might be inferred from the authenticated context or be system-initiated.
|
| 19 |
+
private UUID requestedByUserId;
|
| 20 |
+
|
| 21 |
+
}
|
src/main/java/com/dalab/reporting/event/PolicyActionEventDTO.java
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.event;
|
| 2 |
+
|
| 3 |
+
import java.time.Instant;
|
| 4 |
+
import java.util.Map;
|
| 5 |
+
import java.util.UUID;
|
| 6 |
+
|
| 7 |
+
import lombok.Data;
|
| 8 |
+
import lombok.NoArgsConstructor;
|
| 9 |
+
|
| 10 |
+
// Placeholder DTO - ideally this would come from da-protos or a shared library
|
| 11 |
+
@Data
|
| 12 |
+
@NoArgsConstructor
|
| 13 |
+
public class PolicyActionEventDTO {
|
| 14 |
+
private UUID eventId;
|
| 15 |
+
private Instant eventTimestamp;
|
| 16 |
+
private String policyId;
|
| 17 |
+
private String policyName;
|
| 18 |
+
private String ruleId; // Optional, if action is rule-specific
|
| 19 |
+
private String ruleName; // Optional
|
| 20 |
+
private UUID targetAssetId;
|
| 21 |
+
private String actionType; // e.g., "NOTIFY_ADMIN", "AUTO_TAG", "QUARANTINE_ASSET"
|
| 22 |
+
private Map<String, Object> actionParameters; // Specifics for the action
|
| 23 |
+
private String evaluationStatus; // e.g., "FAIL", "WARN"
|
| 24 |
+
private Map<String, Object> evaluationDetails; // e.g., what conditions triggered it
|
| 25 |
+
}
|
src/main/java/com/dalab/reporting/kafka/PolicyActionConsumer.java
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.kafka;
|
| 2 |
+
|
| 3 |
+
import java.util.HashMap;
|
| 4 |
+
import java.util.List;
|
| 5 |
+
import java.util.Map;
|
| 6 |
+
|
| 7 |
+
import org.slf4j.Logger;
|
| 8 |
+
import org.slf4j.LoggerFactory;
|
| 9 |
+
import org.springframework.beans.factory.annotation.Autowired;
|
| 10 |
+
import org.springframework.kafka.annotation.KafkaListener;
|
| 11 |
+
import org.springframework.messaging.handler.annotation.Payload;
|
| 12 |
+
import org.springframework.stereotype.Service;
|
| 13 |
+
|
| 14 |
+
import com.dalab.reporting.dto.NotificationRequestDTO;
|
| 15 |
+
import com.dalab.reporting.model.NotificationChannel;
|
| 16 |
+
import com.dalab.reporting.service.INotificationService;
|
| 17 |
+
|
| 18 |
+
@Service
|
| 19 |
+
public class PolicyActionConsumer {
|
| 20 |
+
|
| 21 |
+
private static final Logger log = LoggerFactory.getLogger(PolicyActionConsumer.class);
|
| 22 |
+
|
| 23 |
+
private final INotificationService notificationService;
|
| 24 |
+
|
| 25 |
+
@Autowired
|
| 26 |
+
public PolicyActionConsumer(INotificationService notificationService) {
|
| 27 |
+
this.notificationService = notificationService;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// Updated to consume Map-based events instead of protobuf
|
| 31 |
+
@KafkaListener(topics = "${app.kafka.topic.policy-action-event:dalab.policies.actions}",
|
| 32 |
+
groupId = "${spring.kafka.consumer.group-id}")
|
| 33 |
+
public void handlePolicyActionEvent(@Payload Map<String, Object> event) {
|
| 34 |
+
log.info("Received PolicyActionEvent: AssetId={}, ActionType={}",
|
| 35 |
+
event.get("assetId"), event.get("actionType"));
|
| 36 |
+
|
| 37 |
+
String actionType = (String) event.get("actionType");
|
| 38 |
+
String policyId = (String) event.get("policyId");
|
| 39 |
+
String assetId = (String) event.get("assetId");
|
| 40 |
+
|
| 41 |
+
if (actionType != null && actionType.toUpperCase().startsWith("NOTIFY")) {
|
| 42 |
+
String subject = String.format("Policy Alert on Asset %s (Policy: %s)", assetId, policyId);
|
| 43 |
+
|
| 44 |
+
StringBuilder body = new StringBuilder();
|
| 45 |
+
body.append(String.format("Policy ID '%s' triggered action: %s\n", policyId, actionType));
|
| 46 |
+
body.append(String.format("Target Asset ID: %s\n", assetId));
|
| 47 |
+
if (event.containsKey("evaluationId")) {
|
| 48 |
+
body.append(String.format("Evaluation ID: %s\n", event.get("evaluationId")));
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
Map<String, Object> params = event;
|
| 52 |
+
if (params != null && !params.isEmpty()) {
|
| 53 |
+
body.append("Action Parameters:\n");
|
| 54 |
+
params.forEach((k, v) -> {
|
| 55 |
+
body.append(String.format(" %s: %s\n", k, valueToString(v)));
|
| 56 |
+
});
|
| 57 |
+
}
|
| 58 |
+
if (event.containsKey("eventTimestamp")) {
|
| 59 |
+
String eventTimestamp = (String) event.get("eventTimestamp");
|
| 60 |
+
body.append(String.format("Event Timestamp: %s\n", eventTimestamp));
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
NotificationRequestDTO.NotificationRequestDTOBuilder requestBuilder = NotificationRequestDTO.builder()
|
| 64 |
+
.subject(subject)
|
| 65 |
+
.body(body.toString());
|
| 66 |
+
|
| 67 |
+
Map<String, Object> contentModelForTemplate = new HashMap<>();
|
| 68 |
+
contentModelForTemplate.put("policyId", policyId);
|
| 69 |
+
contentModelForTemplate.put("assetId", assetId);
|
| 70 |
+
contentModelForTemplate.put("actionType", actionType);
|
| 71 |
+
if (event.containsKey("evaluationId")) contentModelForTemplate.put("evaluationId", event.get("evaluationId"));
|
| 72 |
+
|
| 73 |
+
Map<String, String> simpleParams = new HashMap<>();
|
| 74 |
+
params.forEach((k,v) -> simpleParams.put(k, valueToString(v)));
|
| 75 |
+
contentModelForTemplate.put("actionParameters", simpleParams);
|
| 76 |
+
if (event.containsKey("eventTimestamp")) {
|
| 77 |
+
String eventTimestamp = (String) event.get("eventTimestamp");
|
| 78 |
+
contentModelForTemplate.put("eventTimestamp", eventTimestamp);
|
| 79 |
+
}
|
| 80 |
+
requestBuilder.contentModel(contentModelForTemplate);
|
| 81 |
+
|
| 82 |
+
String notificationKeyForPolicyAlerts = "POLICY_ALERT";
|
| 83 |
+
Object notificationKeyValue = params.get("notificationTypeKey");
|
| 84 |
+
if (notificationKeyValue instanceof String) {
|
| 85 |
+
notificationKeyForPolicyAlerts = (String) notificationKeyValue;
|
| 86 |
+
} else {
|
| 87 |
+
log.warn("No specific notificationTypeKey in actionParameters for policy alert, using default key: {}", notificationKeyForPolicyAlerts);
|
| 88 |
+
}
|
| 89 |
+
requestBuilder.notificationTypeKey(notificationKeyForPolicyAlerts);
|
| 90 |
+
|
| 91 |
+
Object channelsValue = params.get("channels");
|
| 92 |
+
if (channelsValue instanceof List) {
|
| 93 |
+
try {
|
| 94 |
+
List<String> channelNames = (List<String>) channelsValue;
|
| 95 |
+
requestBuilder.channels(channelNames.stream().map(s -> NotificationChannel.valueOf(s.toUpperCase())).toList());
|
| 96 |
+
} catch (Exception e) {
|
| 97 |
+
log.warn("Could not parse 'channels' from actionParameters, defaulting to EMAIL and SLACK. Error: {}", e.getMessage());
|
| 98 |
+
requestBuilder.channels(List.of(NotificationChannel.EMAIL, NotificationChannel.SLACK));
|
| 99 |
+
}
|
| 100 |
+
} else {
|
| 101 |
+
requestBuilder.channels(List.of(NotificationChannel.EMAIL, NotificationChannel.SLACK));
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
try {
|
| 105 |
+
notificationService.sendNotification(requestBuilder.build());
|
| 106 |
+
log.info("Notification sent for PolicyActionEvent on assetId: {}", assetId);
|
| 107 |
+
} catch (Exception e) {
|
| 108 |
+
log.error("Failed to send notification for PolicyActionEvent on assetId {}: {}", assetId, e.getMessage(), e);
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// TODO: Add logic here to feed data into internal reporting tables based on the event.
|
| 113 |
+
// e.g., increment compliance failure counts, log governance actions, etc.
|
| 114 |
+
// This would involve new JPA entities/repositories for aggregated report data.
|
| 115 |
+
log.info("Further processing for PolicyActionEvent on assetId {} (e.g. report data aggregation) can be added here.", assetId);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// Helper method to convert protobuf Value to String for logging/display
|
| 119 |
+
private String valueToString(Object value) {
|
| 120 |
+
if (value == null) return "null";
|
| 121 |
+
if (value instanceof String) return (String) value;
|
| 122 |
+
if (value instanceof Number) return value.toString();
|
| 123 |
+
if (value instanceof Boolean) return Boolean.toString((Boolean) value);
|
| 124 |
+
if (value instanceof Map) {
|
| 125 |
+
StringBuilder sb = new StringBuilder();
|
| 126 |
+
sb.append("{");
|
| 127 |
+
((Map<?, ?>) value).forEach((k, v) -> {
|
| 128 |
+
sb.append("\"").append(k).append("\": ").append(valueToString(v)).append(", ");
|
| 129 |
+
});
|
| 130 |
+
sb.append("}");
|
| 131 |
+
return sb.toString();
|
| 132 |
+
}
|
| 133 |
+
if (value instanceof List) {
|
| 134 |
+
StringBuilder sb = new StringBuilder();
|
| 135 |
+
sb.append("[");
|
| 136 |
+
for (Object item : (List<?>) value) {
|
| 137 |
+
sb.append(valueToString(item)).append(", ");
|
| 138 |
+
}
|
| 139 |
+
sb.append("]");
|
| 140 |
+
return sb.toString();
|
| 141 |
+
}
|
| 142 |
+
return "<unknown_value_type>";
|
| 143 |
+
}
|
| 144 |
+
}
|
src/main/java/com/dalab/reporting/mapper/NotificationPreferenceMapper.java
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.mapper;
|
| 2 |
+
|
| 3 |
+
import java.util.List;
|
| 4 |
+
|
| 5 |
+
import org.mapstruct.Mapper;
|
| 6 |
+
import org.mapstruct.Mapping;
|
| 7 |
+
import org.mapstruct.factory.Mappers;
|
| 8 |
+
|
| 9 |
+
import com.dalab.reporting.dto.NotificationPreferenceDTO;
|
| 10 |
+
import com.dalab.reporting.model.UserNotificationPreference;
|
| 11 |
+
|
| 12 |
+
@Mapper(componentModel = "spring")
|
| 13 |
+
public interface NotificationPreferenceMapper {
|
| 14 |
+
|
| 15 |
+
NotificationPreferenceMapper INSTANCE = Mappers.getMapper(NotificationPreferenceMapper.class);
|
| 16 |
+
|
| 17 |
+
NotificationPreferenceDTO toDTO(UserNotificationPreference entity);
|
| 18 |
+
UserNotificationPreference toEntity(NotificationPreferenceDTO dto);
|
| 19 |
+
List<NotificationPreferenceDTO> toDTOList(List<UserNotificationPreference> entityList);
|
| 20 |
+
}
|
src/main/java/com/dalab/reporting/mapper/ReportMapper.java
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.mapper;
|
| 2 |
+
|
| 3 |
+
import com.dalab.reporting.dto.GeneratedReportOutputDTO;
|
| 4 |
+
import com.dalab.reporting.dto.ReportDefinitionDTO;
|
| 5 |
+
import com.dalab.reporting.model.GeneratedReport;
|
| 6 |
+
import com.dalab.reporting.model.ReportDefinition;
|
| 7 |
+
import org.mapstruct.Mapper;
|
| 8 |
+
import org.mapstruct.Mapping;
|
| 9 |
+
import org.mapstruct.factory.Mappers;
|
| 10 |
+
|
| 11 |
+
import java.util.List;
|
| 12 |
+
|
| 13 |
+
@Mapper(componentModel = "spring")
|
| 14 |
+
public interface ReportMapper {
|
| 15 |
+
|
| 16 |
+
ReportMapper INSTANCE = Mappers.getMapper(ReportMapper.class);
|
| 17 |
+
|
| 18 |
+
// ReportDefinition Mappings
|
| 19 |
+
ReportDefinitionDTO toDTO(ReportDefinition entity);
|
| 20 |
+
ReportDefinition toEntity(ReportDefinitionDTO dto);
|
| 21 |
+
List<ReportDefinitionDTO> toDTOList(List<ReportDefinition> entityList);
|
| 22 |
+
|
| 23 |
+
// GeneratedReport Mappings
|
| 24 |
+
@Mapping(source = "reportDefinition.reportKey", target = "reportKey")
|
| 25 |
+
@Mapping(source = "reportDefinition.displayName", target = "reportDisplayName")
|
| 26 |
+
GeneratedReportOutputDTO toDTO(GeneratedReport entity);
|
| 27 |
+
List<GeneratedReportOutputDTO> toGeneratedReportDTOList(List<GeneratedReport> entityList);
|
| 28 |
+
|
| 29 |
+
// We typically don't map from GeneratedReportOutputDTO back to GeneratedReport entity directly,
|
| 30 |
+
// as creation/update paths for GeneratedReport are usually more complex and handled by services.
|
| 31 |
+
}
|
src/main/java/com/dalab/reporting/model/GeneratedReport.java
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.model;
|
| 2 |
+
|
| 3 |
+
import java.time.Instant;
|
| 4 |
+
import java.util.Map;
|
| 5 |
+
import java.util.UUID;
|
| 6 |
+
|
| 7 |
+
import org.hibernate.annotations.JdbcTypeCode;
|
| 8 |
+
import org.hibernate.type.SqlTypes;
|
| 9 |
+
|
| 10 |
+
import jakarta.persistence.*;
|
| 11 |
+
import jakarta.validation.constraints.NotNull;
|
| 12 |
+
|
| 13 |
+
@Entity
|
| 14 |
+
@Table(name = "generated_reports")
|
| 15 |
+
public class GeneratedReport {
|
| 16 |
+
|
| 17 |
+
@Id
|
| 18 |
+
@GeneratedValue(strategy = GenerationType.AUTO)
|
| 19 |
+
@Column(columnDefinition = "UUID")
|
| 20 |
+
private UUID id;
|
| 21 |
+
|
| 22 |
+
@NotNull
|
| 23 |
+
@ManyToOne(fetch = FetchType.LAZY)
|
| 24 |
+
@JoinColumn(name = "report_definition_id", nullable = false)
|
| 25 |
+
private ReportDefinition reportDefinition;
|
| 26 |
+
|
| 27 |
+
@Enumerated(EnumType.STRING)
|
| 28 |
+
@Column(nullable = false)
|
| 29 |
+
private ReportStatus status;
|
| 30 |
+
|
| 31 |
+
// Parameters used for this specific generation instance
|
| 32 |
+
@JdbcTypeCode(SqlTypes.JSON)
|
| 33 |
+
@Column(columnDefinition = "jsonb")
|
| 34 |
+
private Map<String, Object> generationParameters;
|
| 35 |
+
|
| 36 |
+
// Store the report content directly if it's small (e.g., JSON/CSV snippet)
|
| 37 |
+
// For larger reports, this might store a path/URI to the report file (e.g., S3 link)
|
| 38 |
+
@Lob
|
| 39 |
+
@JdbcTypeCode(SqlTypes.JSON) // Or SqlTypes.VARCHAR for path, or SqlTypes.BINARY for blob
|
| 40 |
+
@Column(columnDefinition = "jsonb") // Or TEXT / BYTEA depending on content type
|
| 41 |
+
private String reportContent; // Could be JSON, CSV string, or a URI
|
| 42 |
+
|
| 43 |
+
@Column(columnDefinition = "TEXT")
|
| 44 |
+
private String failureReason; // If status is FAILED
|
| 45 |
+
|
| 46 |
+
@Column(nullable = false, updatable = false)
|
| 47 |
+
private Instant requestedAt; // When the report generation was requested/scheduled
|
| 48 |
+
|
| 49 |
+
private Instant startedAt;
|
| 50 |
+
private Instant completedAt;
|
| 51 |
+
|
| 52 |
+
// Could be the user ID who requested it, or system if scheduled
|
| 53 |
+
private UUID requestedByUserId;
|
| 54 |
+
|
| 55 |
+
// Getters and Setters
|
| 56 |
+
|
| 57 |
+
public UUID getId() {
|
| 58 |
+
return id;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
public void setId(UUID id) {
|
| 62 |
+
this.id = id;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
public ReportDefinition getReportDefinition() {
|
| 66 |
+
return reportDefinition;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
public void setReportDefinition(ReportDefinition reportDefinition) {
|
| 70 |
+
this.reportDefinition = reportDefinition;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
public ReportStatus getStatus() {
|
| 74 |
+
return status;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
public void setStatus(ReportStatus status) {
|
| 78 |
+
this.status = status;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
public Map<String, Object> getGenerationParameters() {
|
| 82 |
+
return generationParameters;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
public void setGenerationParameters(Map<String, Object> generationParameters) {
|
| 86 |
+
this.generationParameters = generationParameters;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
public String getReportContent() {
|
| 90 |
+
return reportContent;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
public void setReportContent(String reportContent) {
|
| 94 |
+
this.reportContent = reportContent;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
public String getFailureReason() {
|
| 98 |
+
return failureReason;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
public void setFailureReason(String failureReason) {
|
| 102 |
+
this.failureReason = failureReason;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
public Instant getRequestedAt() {
|
| 106 |
+
return requestedAt;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
public void setRequestedAt(Instant requestedAt) {
|
| 110 |
+
this.requestedAt = requestedAt;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
public Instant getStartedAt() {
|
| 114 |
+
return startedAt;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
public void setStartedAt(Instant startedAt) {
|
| 118 |
+
this.startedAt = startedAt;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
public Instant getCompletedAt() {
|
| 122 |
+
return completedAt;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
public void setCompletedAt(Instant completedAt) {
|
| 126 |
+
this.completedAt = completedAt;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
public UUID getRequestedByUserId() {
|
| 130 |
+
return requestedByUserId;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
public void setRequestedByUserId(UUID requestedByUserId) {
|
| 134 |
+
this.requestedByUserId = requestedByUserId;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
@PrePersist
|
| 138 |
+
protected void onCreate() {
|
| 139 |
+
if (requestedAt == null) {
|
| 140 |
+
requestedAt = Instant.now();
|
| 141 |
+
}
|
| 142 |
+
if (status == null) {
|
| 143 |
+
status = ReportStatus.PENDING;
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
}
|
src/main/java/com/dalab/reporting/model/NotificationChannel.java
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.model;
|
| 2 |
+
|
| 3 |
+
public enum NotificationChannel {
|
| 4 |
+
EMAIL,
|
| 5 |
+
SLACK,
|
| 6 |
+
// IN_APP, // Future: for notifications within a UI
|
| 7 |
+
// TEAMS // Future: Microsoft Teams
|
| 8 |
+
}
|
src/main/java/com/dalab/reporting/model/ReportDefinition.java
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.model;
|
| 2 |
+
|
| 3 |
+
import java.time.Instant;
|
| 4 |
+
import java.util.Map;
|
| 5 |
+
import java.util.UUID;
|
| 6 |
+
|
| 7 |
+
import org.hibernate.annotations.JdbcTypeCode;
|
| 8 |
+
import org.hibernate.type.SqlTypes;
|
| 9 |
+
|
| 10 |
+
import jakarta.persistence.*;
|
| 11 |
+
import jakarta.validation.constraints.NotBlank;
|
| 12 |
+
import jakarta.validation.constraints.Size;
|
| 13 |
+
|
| 14 |
+
@Entity
|
| 15 |
+
@Table(name = "report_definitions")
|
| 16 |
+
public class ReportDefinition {
|
| 17 |
+
|
| 18 |
+
@Id
|
| 19 |
+
@GeneratedValue(strategy = GenerationType.AUTO)
|
| 20 |
+
@Column(columnDefinition = "UUID")
|
| 21 |
+
private UUID id;
|
| 22 |
+
|
| 23 |
+
@NotBlank
|
| 24 |
+
@Size(max = 100)
|
| 25 |
+
@Column(nullable = false, unique = true)
|
| 26 |
+
private String reportKey; // e.g., "COST_OPTIMIZATION", "COMPLIANCE_POSTURE"
|
| 27 |
+
|
| 28 |
+
@NotBlank
|
| 29 |
+
@Size(max = 255)
|
| 30 |
+
@Column(nullable = false)
|
| 31 |
+
private String displayName;
|
| 32 |
+
|
| 33 |
+
@Size(max = 1000)
|
| 34 |
+
private String description;
|
| 35 |
+
|
| 36 |
+
// Default parameters for generating this report, e.g., time range, specific filters
|
| 37 |
+
@JdbcTypeCode(SqlTypes.JSON)
|
| 38 |
+
@Column(columnDefinition = "jsonb")
|
| 39 |
+
private Map<String, Object> defaultParameters;
|
| 40 |
+
|
| 41 |
+
// Default cron schedule for automated generation, can be null if only manually triggered
|
| 42 |
+
@Size(max = 100)
|
| 43 |
+
private String defaultScheduleCron;
|
| 44 |
+
|
| 45 |
+
private boolean enabled = true; // Whether this report type is generally available
|
| 46 |
+
|
| 47 |
+
@Column(nullable = false, updatable = false)
|
| 48 |
+
private Instant createdAt;
|
| 49 |
+
|
| 50 |
+
private Instant updatedAt;
|
| 51 |
+
|
| 52 |
+
// Getters and Setters
|
| 53 |
+
public UUID getId() {
|
| 54 |
+
return id;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
public void setId(UUID id) {
|
| 58 |
+
this.id = id;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
public String getReportKey() {
|
| 62 |
+
return reportKey;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
public void setReportKey(String reportKey) {
|
| 66 |
+
this.reportKey = reportKey;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
public String getDisplayName() {
|
| 70 |
+
return displayName;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
public void setDisplayName(String displayName) {
|
| 74 |
+
this.displayName = displayName;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
public String getDescription() {
|
| 78 |
+
return description;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
public void setDescription(String description) {
|
| 82 |
+
this.description = description;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
public Map<String, Object> getDefaultParameters() {
|
| 86 |
+
return defaultParameters;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
public void setDefaultParameters(Map<String, Object> defaultParameters) {
|
| 90 |
+
this.defaultParameters = defaultParameters;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
public String getDefaultScheduleCron() {
|
| 94 |
+
return defaultScheduleCron;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
public void setDefaultScheduleCron(String defaultScheduleCron) {
|
| 98 |
+
this.defaultScheduleCron = defaultScheduleCron;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
public boolean isEnabled() {
|
| 102 |
+
return enabled;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
public void setEnabled(boolean enabled) {
|
| 106 |
+
this.enabled = enabled;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
public Instant getCreatedAt() {
|
| 110 |
+
return createdAt;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
public void setCreatedAt(Instant createdAt) {
|
| 114 |
+
this.createdAt = createdAt;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
public Instant getUpdatedAt() {
|
| 118 |
+
return updatedAt;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
public void setUpdatedAt(Instant updatedAt) {
|
| 122 |
+
this.updatedAt = updatedAt;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
@PrePersist
|
| 126 |
+
protected void onCreate() {
|
| 127 |
+
createdAt = Instant.now();
|
| 128 |
+
updatedAt = Instant.now();
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
@PreUpdate
|
| 132 |
+
protected void onUpdate() {
|
| 133 |
+
updatedAt = Instant.now();
|
| 134 |
+
}
|
| 135 |
+
}
|
src/main/java/com/dalab/reporting/model/ReportStatus.java
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.model;
|
| 2 |
+
|
| 3 |
+
public enum ReportStatus {
|
| 4 |
+
PENDING, // Report generation has been requested/scheduled but not yet started
|
| 5 |
+
GENERATING, // Report generation is in progress
|
| 6 |
+
COMPLETED, // Report generation finished successfully
|
| 7 |
+
FAILED, // Report generation failed
|
| 8 |
+
CANCELLED // Report generation was cancelled
|
| 9 |
+
}
|
src/main/java/com/dalab/reporting/model/UserNotificationPreference.java
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.model;
|
| 2 |
+
|
| 3 |
+
import jakarta.persistence.*;
|
| 4 |
+
import jakarta.validation.constraints.NotBlank;
|
| 5 |
+
import jakarta.validation.constraints.NotNull;
|
| 6 |
+
import jakarta.validation.constraints.Size;
|
| 7 |
+
import org.hibernate.annotations.JdbcTypeCode;
|
| 8 |
+
import org.hibernate.type.SqlTypes;
|
| 9 |
+
|
| 10 |
+
import java.time.Instant;
|
| 11 |
+
import java.util.Map;
|
| 12 |
+
import java.util.UUID;
|
| 13 |
+
|
| 14 |
+
@Entity
|
| 15 |
+
@Table(name = "user_notification_preferences",
|
| 16 |
+
uniqueConstraints = {
|
| 17 |
+
@UniqueConstraint(columnNames = {"user_id", "notification_type_key", "channel"})
|
| 18 |
+
})
|
| 19 |
+
public class UserNotificationPreference {
|
| 20 |
+
|
| 21 |
+
@Id
|
| 22 |
+
@GeneratedValue(strategy = GenerationType.AUTO)
|
| 23 |
+
@Column(columnDefinition = "UUID")
|
| 24 |
+
private UUID id;
|
| 25 |
+
|
| 26 |
+
@NotNull
|
| 27 |
+
@Column(name = "user_id", nullable = false)
|
| 28 |
+
private UUID userId; // The ID of the user (from Keycloak or central user management)
|
| 29 |
+
|
| 30 |
+
@NotBlank
|
| 31 |
+
@Size(max = 100)
|
| 32 |
+
@Column(name = "notification_type_key", nullable = false)
|
| 33 |
+
private String notificationTypeKey; // e.g., "REPORT_COMPLETED_COST_OPTIMIZATION", "POLICY_ALERT_HIGH_SEVERITY"
|
| 34 |
+
|
| 35 |
+
@NotNull
|
| 36 |
+
@Enumerated(EnumType.STRING)
|
| 37 |
+
@Column(nullable = false)
|
| 38 |
+
private NotificationChannel channel; // EMAIL, SLACK
|
| 39 |
+
|
| 40 |
+
private boolean enabled = true;
|
| 41 |
+
|
| 42 |
+
// Channel-specific details, e.g., for SLACK, it could be { "channelId": "C123", "userId": "U456" }
|
| 43 |
+
// For EMAIL, it might be overridden by a central user profile email but could allow specific addresses here.
|
| 44 |
+
@JdbcTypeCode(SqlTypes.JSON)
|
| 45 |
+
@Column(columnDefinition = "jsonb")
|
| 46 |
+
private Map<String, String> channelConfiguration; // e.g., Slack channel ID, specific email address
|
| 47 |
+
|
| 48 |
+
@Column(nullable = false, updatable = false)
|
| 49 |
+
private Instant createdAt;
|
| 50 |
+
|
| 51 |
+
private Instant updatedAt;
|
| 52 |
+
|
| 53 |
+
// Getters and Setters
|
| 54 |
+
|
| 55 |
+
public UUID getId() {
|
| 56 |
+
return id;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
public void setId(UUID id) {
|
| 60 |
+
this.id = id;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
public UUID getUserId() {
|
| 64 |
+
return userId;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
public void setUserId(UUID userId) {
|
| 68 |
+
this.userId = userId;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
public String getNotificationTypeKey() {
|
| 72 |
+
return notificationTypeKey;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
public void setNotificationTypeKey(String notificationTypeKey) {
|
| 76 |
+
this.notificationTypeKey = notificationTypeKey;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
public NotificationChannel getChannel() {
|
| 80 |
+
return channel;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
public void setChannel(NotificationChannel channel) {
|
| 84 |
+
this.channel = channel;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
public boolean isEnabled() {
|
| 88 |
+
return enabled;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
public void setEnabled(boolean enabled) {
|
| 92 |
+
this.enabled = enabled;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
public Map<String, String> getChannelConfiguration() {
|
| 96 |
+
return channelConfiguration;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
public void setChannelConfiguration(Map<String, String> channelConfiguration) {
|
| 100 |
+
this.channelConfiguration = channelConfiguration;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
public Instant getCreatedAt() {
|
| 104 |
+
return createdAt;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
public void setCreatedAt(Instant createdAt) {
|
| 108 |
+
this.createdAt = createdAt;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
public Instant getUpdatedAt() {
|
| 112 |
+
return updatedAt;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
public void setUpdatedAt(Instant updatedAt) {
|
| 116 |
+
this.updatedAt = updatedAt;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
@PrePersist
|
| 120 |
+
protected void onCreate() {
|
| 121 |
+
createdAt = Instant.now();
|
| 122 |
+
updatedAt = Instant.now();
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
@PreUpdate
|
| 126 |
+
protected void onUpdate() {
|
| 127 |
+
updatedAt = Instant.now();
|
| 128 |
+
}
|
| 129 |
+
}
|
src/main/java/com/dalab/reporting/repository/GeneratedReportRepository.java
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.repository;
|
| 2 |
+
|
| 3 |
+
import java.util.UUID;
|
| 4 |
+
|
| 5 |
+
import org.springframework.data.domain.Page;
|
| 6 |
+
import org.springframework.data.domain.Pageable;
|
| 7 |
+
import org.springframework.data.jpa.repository.JpaRepository;
|
| 8 |
+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
| 9 |
+
import org.springframework.stereotype.Repository;
|
| 10 |
+
|
| 11 |
+
import com.dalab.reporting.model.GeneratedReport;
|
| 12 |
+
import com.dalab.reporting.model.ReportDefinition;
|
| 13 |
+
import com.dalab.reporting.model.ReportStatus;
|
| 14 |
+
|
| 15 |
+
@Repository
|
| 16 |
+
public interface GeneratedReportRepository extends JpaRepository<GeneratedReport, UUID>, JpaSpecificationExecutor<GeneratedReport> {
|
| 17 |
+
Page<GeneratedReport> findByReportDefinition(ReportDefinition reportDefinition, Pageable pageable);
|
| 18 |
+
Page<GeneratedReport> findByStatus(ReportStatus status, Pageable pageable);
|
| 19 |
+
Page<GeneratedReport> findByRequestedByUserId(UUID userId, Pageable pageable);
|
| 20 |
+
}
|
src/main/java/com/dalab/reporting/repository/ReportDefinitionRepository.java
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.repository;
|
| 2 |
+
|
| 3 |
+
import java.util.Optional;
|
| 4 |
+
import java.util.UUID;
|
| 5 |
+
|
| 6 |
+
import org.springframework.data.jpa.repository.JpaRepository;
|
| 7 |
+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
| 8 |
+
import org.springframework.stereotype.Repository;
|
| 9 |
+
|
| 10 |
+
import com.dalab.reporting.model.ReportDefinition;
|
| 11 |
+
|
| 12 |
+
@Repository
|
| 13 |
+
public interface ReportDefinitionRepository extends JpaRepository<ReportDefinition, UUID>, JpaSpecificationExecutor<ReportDefinition> {
|
| 14 |
+
Optional<ReportDefinition> findByReportKey(String reportKey);
|
| 15 |
+
}
|
src/main/java/com/dalab/reporting/repository/UserNotificationPreferenceRepository.java
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.repository;
|
| 2 |
+
|
| 3 |
+
import java.util.List;
|
| 4 |
+
import java.util.Optional;
|
| 5 |
+
import java.util.UUID;
|
| 6 |
+
|
| 7 |
+
import org.springframework.data.jpa.repository.JpaRepository;
|
| 8 |
+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
| 9 |
+
import org.springframework.stereotype.Repository;
|
| 10 |
+
|
| 11 |
+
import com.dalab.reporting.model.NotificationChannel;
|
| 12 |
+
import com.dalab.reporting.model.UserNotificationPreference;
|
| 13 |
+
|
| 14 |
+
@Repository
|
| 15 |
+
public interface UserNotificationPreferenceRepository extends JpaRepository<UserNotificationPreference, UUID>, JpaSpecificationExecutor<UserNotificationPreference> {
|
| 16 |
+
List<UserNotificationPreference> findByUserId(UUID userId);
|
| 17 |
+
Optional<UserNotificationPreference> findByUserIdAndNotificationTypeKeyAndChannel(UUID userId, String notificationTypeKey, NotificationChannel channel);
|
| 18 |
+
List<UserNotificationPreference> findByNotificationTypeKeyAndChannelAndEnabled(String notificationTypeKey, NotificationChannel channel, boolean enabled);
|
| 19 |
+
List<UserNotificationPreference> findByUserIdAndEnabled(UUID userId, boolean enabled);
|
| 20 |
+
}
|
src/main/java/com/dalab/reporting/service/DashboardService.java
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.service;
|
| 2 |
+
|
| 3 |
+
import org.slf4j.Logger;
|
| 4 |
+
import org.slf4j.LoggerFactory;
|
| 5 |
+
import org.springframework.cache.annotation.CacheEvict;
|
| 6 |
+
import org.springframework.stereotype.Service;
|
| 7 |
+
|
| 8 |
+
import com.dalab.reporting.client.CatalogServiceClient;
|
| 9 |
+
import com.dalab.reporting.client.PolicyEngineServiceClient;
|
| 10 |
+
import com.dalab.reporting.dto.ActionItemsDTO;
|
| 11 |
+
import com.dalab.reporting.dto.CostSavingsDTO;
|
| 12 |
+
import com.dalab.reporting.dto.GovernanceScoreDTO;
|
| 13 |
+
|
| 14 |
+
import lombok.RequiredArgsConstructor;
|
| 15 |
+
|
| 16 |
+
import java.math.BigDecimal;
|
| 17 |
+
import java.time.LocalDateTime;
|
| 18 |
+
import java.util.Arrays;
|
| 19 |
+
import java.util.List;
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* Implementation of dashboard aggregation service
|
| 23 |
+
* Provides cross-service data aggregation for role-based dashboards
|
| 24 |
+
*
|
| 25 |
+
* @author DALab Development Team
|
| 26 |
+
* @since 2025-01-02
|
| 27 |
+
*/
|
| 28 |
+
@Service
|
| 29 |
+
@RequiredArgsConstructor
|
| 30 |
+
public class DashboardService implements IDashboardService {
|
| 31 |
+
|
| 32 |
+
private static final Logger log = LoggerFactory.getLogger(DashboardService.class);
|
| 33 |
+
|
| 34 |
+
private final CatalogServiceClient catalogServiceClient;
|
| 35 |
+
private final PolicyEngineServiceClient policyEngineServiceClient;
|
| 36 |
+
|
| 37 |
+
@Override
|
| 38 |
+
public GovernanceScoreDTO getGovernanceScore(Boolean includeHistory, Boolean includeBenchmark) {
|
| 39 |
+
log.info("Calculating governance score with history: {}, benchmark: {}", includeHistory, includeBenchmark);
|
| 40 |
+
|
| 41 |
+
try {
|
| 42 |
+
// TODO: Replace with real cross-service aggregation
|
| 43 |
+
// For Week 1, return comprehensive mock data that matches the expected structure
|
| 44 |
+
|
| 45 |
+
// Mock governance breakdown data
|
| 46 |
+
GovernanceScoreDTO.GovernanceBreakdown breakdown = GovernanceScoreDTO.GovernanceBreakdown.builder()
|
| 47 |
+
.policyComplianceScore(85.5)
|
| 48 |
+
.activePolicies(12)
|
| 49 |
+
.violationsCount(3)
|
| 50 |
+
.complianceRate(92.3)
|
| 51 |
+
.dataClassificationScore(78.2)
|
| 52 |
+
.classifiedAssets(156)
|
| 53 |
+
.totalAssets(200)
|
| 54 |
+
.classificationRate(78.0)
|
| 55 |
+
.metadataCompletenessScore(82.1)
|
| 56 |
+
.assetsWithMetadata(164)
|
| 57 |
+
.assetsWithBusinessContext(145)
|
| 58 |
+
.metadataCompletionRate(82.0)
|
| 59 |
+
.lineageCoverageScore(71.4)
|
| 60 |
+
.assetsWithLineage(143)
|
| 61 |
+
.totalLineageConnections(287)
|
| 62 |
+
.lineageCoverageRate(71.5)
|
| 63 |
+
.build();
|
| 64 |
+
|
| 65 |
+
// Calculate weighted overall score: Policy 30%, Classification 25%, Metadata 25%, Lineage 20%
|
| 66 |
+
double overallScore = (breakdown.getPolicyComplianceScore() * 0.30) +
|
| 67 |
+
(breakdown.getDataClassificationScore() * 0.25) +
|
| 68 |
+
(breakdown.getMetadataCompletenessScore() * 0.25) +
|
| 69 |
+
(breakdown.getLineageCoverageScore() * 0.20);
|
| 70 |
+
|
| 71 |
+
// Mock historical trend data (if requested)
|
| 72 |
+
List<GovernanceScoreDTO.GovernanceTrendPoint> historicalTrend = null;
|
| 73 |
+
if (includeHistory) {
|
| 74 |
+
historicalTrend = Arrays.asList(
|
| 75 |
+
GovernanceScoreDTO.GovernanceTrendPoint.builder()
|
| 76 |
+
.date(LocalDateTime.now().minusDays(30))
|
| 77 |
+
.score(75.2)
|
| 78 |
+
.period("daily")
|
| 79 |
+
.build(),
|
| 80 |
+
GovernanceScoreDTO.GovernanceTrendPoint.builder()
|
| 81 |
+
.date(LocalDateTime.now().minusDays(15))
|
| 82 |
+
.score(78.8)
|
| 83 |
+
.period("daily")
|
| 84 |
+
.build(),
|
| 85 |
+
GovernanceScoreDTO.GovernanceTrendPoint.builder()
|
| 86 |
+
.date(LocalDateTime.now())
|
| 87 |
+
.score(overallScore)
|
| 88 |
+
.period("daily")
|
| 89 |
+
.build()
|
| 90 |
+
);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// Mock top governance issues
|
| 94 |
+
List<GovernanceScoreDTO.GovernanceIssue> topIssues = Arrays.asList(
|
| 95 |
+
GovernanceScoreDTO.GovernanceIssue.builder()
|
| 96 |
+
.category("MISSING_CLASSIFICATION")
|
| 97 |
+
.description("44 assets lack proper data classification labels")
|
| 98 |
+
.severity("HIGH")
|
| 99 |
+
.affectedAssets(44)
|
| 100 |
+
.actionRequired("Assign appropriate classification labels")
|
| 101 |
+
.assignedTeam("DATA_ENGINEERING")
|
| 102 |
+
.build(),
|
| 103 |
+
GovernanceScoreDTO.GovernanceIssue.builder()
|
| 104 |
+
.category("POLICY_VIOLATION")
|
| 105 |
+
.description("3 active policy violations require immediate attention")
|
| 106 |
+
.severity("CRITICAL")
|
| 107 |
+
.affectedAssets(3)
|
| 108 |
+
.actionRequired("Review and remediate policy violations")
|
| 109 |
+
.assignedTeam("COMPLIANCE")
|
| 110 |
+
.build()
|
| 111 |
+
);
|
| 112 |
+
|
| 113 |
+
// Mock benchmark comparison (if requested)
|
| 114 |
+
GovernanceScoreDTO.BenchmarkComparison benchmark = null;
|
| 115 |
+
if (includeBenchmark) {
|
| 116 |
+
benchmark = GovernanceScoreDTO.BenchmarkComparison.builder()
|
| 117 |
+
.industryAverage(72.5)
|
| 118 |
+
.peerAverage(76.8)
|
| 119 |
+
.ranking("GOOD")
|
| 120 |
+
.percentile(78)
|
| 121 |
+
.comparisonNote("Above industry average, approaching peer group excellence")
|
| 122 |
+
.build();
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// Mock recommendations
|
| 126 |
+
List<String> recommendations = Arrays.asList(
|
| 127 |
+
"Focus on improving data lineage coverage to reach 80% target",
|
| 128 |
+
"Implement automated classification for new assets to maintain classification rate",
|
| 129 |
+
"Address critical policy violations within next 48 hours",
|
| 130 |
+
"Enhance metadata completeness for business context information"
|
| 131 |
+
);
|
| 132 |
+
|
| 133 |
+
return GovernanceScoreDTO.builder()
|
| 134 |
+
.overallScore(overallScore)
|
| 135 |
+
.breakdown(breakdown)
|
| 136 |
+
.historicalTrend(historicalTrend)
|
| 137 |
+
.topIssues(topIssues)
|
| 138 |
+
.benchmark(benchmark)
|
| 139 |
+
.recommendations(recommendations)
|
| 140 |
+
.lastCalculated(LocalDateTime.now())
|
| 141 |
+
.dataFreshnessMinutes(2)
|
| 142 |
+
.build();
|
| 143 |
+
|
| 144 |
+
} catch (Exception e) {
|
| 145 |
+
log.error("Error calculating governance score", e);
|
| 146 |
+
throw new RuntimeException("Failed to calculate governance score: " + e.getMessage(), e);
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
@Override
|
| 151 |
+
public CostSavingsDTO getCostSavings(Boolean includeProjections, String timeframe) {
|
| 152 |
+
log.info("Calculating cost savings for timeframe: {}, projections: {}", timeframe, includeProjections);
|
| 153 |
+
|
| 154 |
+
try {
|
| 155 |
+
// TODO: Replace with real cost calculation from archival/deletion services and cloud APIs
|
| 156 |
+
// For Week 1, return comprehensive mock data
|
| 157 |
+
|
| 158 |
+
CostSavingsDTO.CostSavingsBreakdown breakdown = CostSavingsDTO.CostSavingsBreakdown.builder()
|
| 159 |
+
.archivalSavings(new BigDecimal("45750.50"))
|
| 160 |
+
.archivedAssetsCount(234)
|
| 161 |
+
.archivalStorageUsed("12.8 TB")
|
| 162 |
+
.deletionSavings(new BigDecimal("23140.25"))
|
| 163 |
+
.deletedAssetsCount(89)
|
| 164 |
+
.storageReclaimed("8.4 TB")
|
| 165 |
+
.tierOptimizationSavings(new BigDecimal("8920.75"))
|
| 166 |
+
.optimizedAssetsCount(156)
|
| 167 |
+
.computeSavings(new BigDecimal("12580.30"))
|
| 168 |
+
.decommissionedResources(23)
|
| 169 |
+
.platformCosts(new BigDecimal("5200.00"))
|
| 170 |
+
.netSavings(new BigDecimal("85191.80"))
|
| 171 |
+
.build();
|
| 172 |
+
|
| 173 |
+
BigDecimal totalSavings = new BigDecimal("90391.80");
|
| 174 |
+
BigDecimal monthlySavingsRate = new BigDecimal("7532.65");
|
| 175 |
+
|
| 176 |
+
List<CostSavingsDTO.ProviderSavings> providerSavings = Arrays.asList(
|
| 177 |
+
CostSavingsDTO.ProviderSavings.builder()
|
| 178 |
+
.providerName("AWS")
|
| 179 |
+
.savings(new BigDecimal("38250.25"))
|
| 180 |
+
.monthlyRate(new BigDecimal("3187.52"))
|
| 181 |
+
.primarySavingsSource("ARCHIVAL")
|
| 182 |
+
.contributionPercentage(42.3)
|
| 183 |
+
.build(),
|
| 184 |
+
CostSavingsDTO.ProviderSavings.builder()
|
| 185 |
+
.providerName("GCP")
|
| 186 |
+
.savings(new BigDecimal("29140.35"))
|
| 187 |
+
.monthlyRate(new BigDecimal("2428.36"))
|
| 188 |
+
.primarySavingsSource("DELETION")
|
| 189 |
+
.contributionPercentage(32.2)
|
| 190 |
+
.build(),
|
| 191 |
+
CostSavingsDTO.ProviderSavings.builder()
|
| 192 |
+
.providerName("Azure")
|
| 193 |
+
.savings(new BigDecimal("23001.20"))
|
| 194 |
+
.monthlyRate(new BigDecimal("1916.77"))
|
| 195 |
+
.primarySavingsSource("OPTIMIZATION")
|
| 196 |
+
.contributionPercentage(25.5)
|
| 197 |
+
.build()
|
| 198 |
+
);
|
| 199 |
+
|
| 200 |
+
// Mock savings opportunities (if projections requested)
|
| 201 |
+
List<CostSavingsDTO.SavingsOpportunity> savingsOpportunities = null;
|
| 202 |
+
if (includeProjections) {
|
| 203 |
+
savingsOpportunities = Arrays.asList(
|
| 204 |
+
CostSavingsDTO.SavingsOpportunity.builder()
|
| 205 |
+
.opportunityType("ARCHIVAL_CANDIDATE")
|
| 206 |
+
.description("67 datasets eligible for archival to cold storage")
|
| 207 |
+
.potentialSavings(new BigDecimal("18750.00"))
|
| 208 |
+
.monthlyImpact(new BigDecimal("1562.50"))
|
| 209 |
+
.timeframe("30_DAYS")
|
| 210 |
+
.effortLevel("LOW")
|
| 211 |
+
.affectedAssets(67)
|
| 212 |
+
.providerName("AWS")
|
| 213 |
+
.actionRequired("Review and approve archival candidates")
|
| 214 |
+
.build()
|
| 215 |
+
);
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
// Mock historical trend
|
| 219 |
+
List<CostSavingsDTO.SavingsTrendPoint> historicalTrend = Arrays.asList(
|
| 220 |
+
CostSavingsDTO.SavingsTrendPoint.builder()
|
| 221 |
+
.month(LocalDateTime.now().minusMonths(2))
|
| 222 |
+
.cumulativeSavings(new BigDecimal("45230.50"))
|
| 223 |
+
.monthlySavings(new BigDecimal("6200.25"))
|
| 224 |
+
.platformCosts(new BigDecimal("4800.00"))
|
| 225 |
+
.netSavings(new BigDecimal("40430.50"))
|
| 226 |
+
.assetsProcessed(156)
|
| 227 |
+
.build(),
|
| 228 |
+
CostSavingsDTO.SavingsTrendPoint.builder()
|
| 229 |
+
.month(LocalDateTime.now().minusMonths(1))
|
| 230 |
+
.cumulativeSavings(new BigDecimal("67820.75"))
|
| 231 |
+
.monthlySavings(new BigDecimal("7120.40"))
|
| 232 |
+
.platformCosts(new BigDecimal("5000.00"))
|
| 233 |
+
.netSavings(new BigDecimal("62820.75"))
|
| 234 |
+
.assetsProcessed(201)
|
| 235 |
+
.build(),
|
| 236 |
+
CostSavingsDTO.SavingsTrendPoint.builder()
|
| 237 |
+
.month(LocalDateTime.now())
|
| 238 |
+
.cumulativeSavings(totalSavings)
|
| 239 |
+
.monthlySavings(monthlySavingsRate)
|
| 240 |
+
.platformCosts(new BigDecimal("5200.00"))
|
| 241 |
+
.netSavings(breakdown.getNetSavings())
|
| 242 |
+
.assetsProcessed(323)
|
| 243 |
+
.build()
|
| 244 |
+
);
|
| 245 |
+
|
| 246 |
+
List<String> recommendations = Arrays.asList(
|
| 247 |
+
"Prioritize archival of large datasets with low access frequency",
|
| 248 |
+
"Review deletion candidates for cost optimization potential",
|
| 249 |
+
"Consider automated tier optimization for frequently accessed data",
|
| 250 |
+
"Monitor platform costs vs savings ratio for ROI optimization"
|
| 251 |
+
);
|
| 252 |
+
|
| 253 |
+
double roiPercentage = (breakdown.getNetSavings().doubleValue() / breakdown.getPlatformCosts().doubleValue()) * 100;
|
| 254 |
+
|
| 255 |
+
return CostSavingsDTO.builder()
|
| 256 |
+
.totalSavings(totalSavings)
|
| 257 |
+
.monthlySavingsRate(monthlySavingsRate)
|
| 258 |
+
.roiPercentage(roiPercentage)
|
| 259 |
+
.breakdown(breakdown)
|
| 260 |
+
.providerSavings(providerSavings)
|
| 261 |
+
.savingsOpportunities(savingsOpportunities)
|
| 262 |
+
.historicalTrend(historicalTrend)
|
| 263 |
+
.recommendations(recommendations)
|
| 264 |
+
.lastCalculated(LocalDateTime.now())
|
| 265 |
+
.dataAccuracy("ESTIMATED")
|
| 266 |
+
.build();
|
| 267 |
+
|
| 268 |
+
} catch (Exception e) {
|
| 269 |
+
log.error("Error calculating cost savings", e);
|
| 270 |
+
throw new RuntimeException("Failed to calculate cost savings: " + e.getMessage(), e);
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
@Override
|
| 275 |
+
public ActionItemsDTO getActionItems(Boolean assignedToUser, String severity, String status, Integer limit) {
|
| 276 |
+
log.info("Aggregating action items with filters - assignedToUser: {}, severity: {}, status: {}, limit: {}",
|
| 277 |
+
assignedToUser, severity, status, limit);
|
| 278 |
+
|
| 279 |
+
try {
|
| 280 |
+
// TODO: Replace with real aggregation from all DALab services
|
| 281 |
+
// For Week 1, return comprehensive mock data
|
| 282 |
+
|
| 283 |
+
ActionItemsDTO.ActionItemsSummary summary = ActionItemsDTO.ActionItemsSummary.builder()
|
| 284 |
+
.criticalCount(4)
|
| 285 |
+
.highCount(12)
|
| 286 |
+
.mediumCount(23)
|
| 287 |
+
.lowCount(8)
|
| 288 |
+
.overdueCount(3)
|
| 289 |
+
.dueTodayCount(7)
|
| 290 |
+
.dueThisWeekCount(15)
|
| 291 |
+
.assignedCount(35)
|
| 292 |
+
.unassignedCount(12)
|
| 293 |
+
.inProgressCount(18)
|
| 294 |
+
.completedCount(156)
|
| 295 |
+
.build();
|
| 296 |
+
|
| 297 |
+
// Mock action items
|
| 298 |
+
List<ActionItemsDTO.ActionItem> actionItems = Arrays.asList(
|
| 299 |
+
ActionItemsDTO.ActionItem.builder()
|
| 300 |
+
.id("AI-001")
|
| 301 |
+
.title("Critical policy violation in customer database")
|
| 302 |
+
.description("PII policy violation detected in customer_data_prod table")
|
| 303 |
+
.category("POLICY_BREACH")
|
| 304 |
+
.severity("CRITICAL")
|
| 305 |
+
.status("OPEN")
|
| 306 |
+
.sourceService("da-policyengine")
|
| 307 |
+
.sourceEntityType("ASSET")
|
| 308 |
+
.sourceEntityId("asset-12345")
|
| 309 |
+
.sourceEntityName("customer_data_prod")
|
| 310 |
+
.assignedTo("john.doe@company.com")
|
| 311 |
+
.assignedTeam("COMPLIANCE")
|
| 312 |
+
.createdAt(LocalDateTime.now().minusHours(2))
|
| 313 |
+
.dueDate(LocalDateTime.now().plusHours(22))
|
| 314 |
+
.lastUpdated(LocalDateTime.now().minusMinutes(30))
|
| 315 |
+
.actionRequired("Review and apply data masking policy")
|
| 316 |
+
.businessJustification("Ensure GDPR compliance for customer data")
|
| 317 |
+
.estimatedEffort("HOURS")
|
| 318 |
+
.businessImpactScore(9)
|
| 319 |
+
.tags(Arrays.asList("GDPR", "PII", "URGENT"))
|
| 320 |
+
.detailsUrl("/policy-engine/violations/POL-001")
|
| 321 |
+
.build(),
|
| 322 |
+
ActionItemsDTO.ActionItem.builder()
|
| 323 |
+
.id("AI-002")
|
| 324 |
+
.title("Missing metadata for analytics datasets")
|
| 325 |
+
.description("44 datasets lack business metadata and data steward assignment")
|
| 326 |
+
.category("METADATA_MISSING")
|
| 327 |
+
.severity("HIGH")
|
| 328 |
+
.status("IN_PROGRESS")
|
| 329 |
+
.sourceService("da-catalog")
|
| 330 |
+
.sourceEntityType("ASSET")
|
| 331 |
+
.sourceEntityId("batch-meta-001")
|
| 332 |
+
.sourceEntityName("Analytics Dataset Batch")
|
| 333 |
+
.assignedTo("jane.smith@company.com")
|
| 334 |
+
.assignedTeam("DATA_ENGINEERING")
|
| 335 |
+
.createdAt(LocalDateTime.now().minusDays(3))
|
| 336 |
+
.dueDate(LocalDateTime.now().plusDays(4))
|
| 337 |
+
.lastUpdated(LocalDateTime.now().minusHours(6))
|
| 338 |
+
.actionRequired("Complete metadata forms for all analytics datasets")
|
| 339 |
+
.businessJustification("Improve data discoverability and governance")
|
| 340 |
+
.estimatedEffort("DAYS")
|
| 341 |
+
.businessImpactScore(6)
|
| 342 |
+
.tags(Arrays.asList("METADATA", "GOVERNANCE"))
|
| 343 |
+
.detailsUrl("/catalog/metadata-gaps/MDG-001")
|
| 344 |
+
.build()
|
| 345 |
+
);
|
| 346 |
+
|
| 347 |
+
// Mock SLA metrics
|
| 348 |
+
ActionItemsDTO.SlaMetrics slaMetrics = ActionItemsDTO.SlaMetrics.builder()
|
| 349 |
+
.averageResolutionTimeHours(24.5)
|
| 350 |
+
.slaComplianceRate(87.3)
|
| 351 |
+
.breachedSlaCount(6)
|
| 352 |
+
.nearBreachCount(3)
|
| 353 |
+
.complianceViolations(ActionItemsDTO.SlaByCategory.builder()
|
| 354 |
+
.totalItems(8)
|
| 355 |
+
.onTimeResolved(6)
|
| 356 |
+
.breachedSla(2)
|
| 357 |
+
.averageResolutionHours(18.5)
|
| 358 |
+
.complianceRate(75.0)
|
| 359 |
+
.build())
|
| 360 |
+
.build();
|
| 361 |
+
|
| 362 |
+
// Mock business impact summary
|
| 363 |
+
ActionItemsDTO.BusinessImpactSummary businessImpact = ActionItemsDTO.BusinessImpactSummary.builder()
|
| 364 |
+
.highImpactItems(4)
|
| 365 |
+
.mediumImpactItems(23)
|
| 366 |
+
.lowImpactItems(20)
|
| 367 |
+
.averageImpactScore(6.2)
|
| 368 |
+
.highestImpactCategory("DATA_PRIVACY")
|
| 369 |
+
.impactAreas(Arrays.asList(
|
| 370 |
+
ActionItemsDTO.ImpactArea.builder()
|
| 371 |
+
.areaName("DATA_PRIVACY")
|
| 372 |
+
.affectedItems(8)
|
| 373 |
+
.averageImpactScore(8.5)
|
| 374 |
+
.riskLevel("HIGH")
|
| 375 |
+
.description("Privacy compliance and PII protection issues")
|
| 376 |
+
.build(),
|
| 377 |
+
ActionItemsDTO.ImpactArea.builder()
|
| 378 |
+
.areaName("OPERATIONAL_EFFICIENCY")
|
| 379 |
+
.affectedItems(25)
|
| 380 |
+
.averageImpactScore(5.2)
|
| 381 |
+
.riskLevel("MEDIUM")
|
| 382 |
+
.description("Metadata and discovery efficiency improvements")
|
| 383 |
+
.build()
|
| 384 |
+
))
|
| 385 |
+
.build();
|
| 386 |
+
|
| 387 |
+
return ActionItemsDTO.builder()
|
| 388 |
+
.totalActionItems(47)
|
| 389 |
+
.summary(summary)
|
| 390 |
+
.actionItems(actionItems)
|
| 391 |
+
.slaMetrics(slaMetrics)
|
| 392 |
+
.businessImpact(businessImpact)
|
| 393 |
+
.lastAggregated(LocalDateTime.now())
|
| 394 |
+
.includedServices(Arrays.asList("da-autocompliance", "da-policyengine", "da-catalog", "da-discovery"))
|
| 395 |
+
.build();
|
| 396 |
+
|
| 397 |
+
} catch (Exception e) {
|
| 398 |
+
log.error("Error aggregating action items", e);
|
| 399 |
+
throw new RuntimeException("Failed to aggregate action items: " + e.getMessage(), e);
|
| 400 |
+
}
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
@Override
|
| 404 |
+
@CacheEvict(value = {"governanceScore", "costSavings", "actionItems"}, allEntries = true)
|
| 405 |
+
public void refreshAllCaches() {
|
| 406 |
+
log.info("Refreshing all dashboard caches");
|
| 407 |
+
// Cache eviction handled by annotation
|
| 408 |
+
log.info("Dashboard caches refreshed successfully");
|
| 409 |
+
}
|
| 410 |
+
}
|
src/main/java/com/dalab/reporting/service/IDashboardService.java
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.service;
|
| 2 |
+
|
| 3 |
+
import com.dalab.reporting.dto.ActionItemsDTO;
|
| 4 |
+
import com.dalab.reporting.dto.CostSavingsDTO;
|
| 5 |
+
import com.dalab.reporting.dto.GovernanceScoreDTO;
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Service interface for DALab Dashboard Aggregation operations
|
| 9 |
+
* Provides cross-service data aggregation for role-based dashboards
|
| 10 |
+
*
|
| 11 |
+
* @author DALab Development Team
|
| 12 |
+
* @since 2025-01-02
|
| 13 |
+
*/
|
| 14 |
+
public interface IDashboardService {
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Calculate comprehensive governance score with weighted metrics
|
| 18 |
+
* Aggregates data from da-catalog, da-autocompliance, and da-policyengine
|
| 19 |
+
*
|
| 20 |
+
* @param includeHistory whether to include 30-day historical trend data
|
| 21 |
+
* @param includeBenchmark whether to include industry benchmark comparison
|
| 22 |
+
* @return governance score with breakdown, trends, and recommendations
|
| 23 |
+
*/
|
| 24 |
+
GovernanceScoreDTO getGovernanceScore(Boolean includeHistory, Boolean includeBenchmark);
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Calculate comprehensive cost savings analysis
|
| 28 |
+
* Aggregates data from da-autoarchival, da-autodelete, and cloud provider APIs
|
| 29 |
+
*
|
| 30 |
+
* @param includeProjections whether to include future savings projections
|
| 31 |
+
* @param timeframe analysis timeframe: "30_DAYS", "90_DAYS", "12_MONTHS", "ALL_TIME"
|
| 32 |
+
* @return cost savings analysis with ROI calculation and opportunities
|
| 33 |
+
*/
|
| 34 |
+
CostSavingsDTO getCostSavings(Boolean includeProjections, String timeframe);
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* Aggregate action items from all DALab services
|
| 38 |
+
* Collects data from da-autocompliance, da-policyengine, da-catalog, da-discovery
|
| 39 |
+
*
|
| 40 |
+
* @param assignedToUser filter to show only items assigned to current user
|
| 41 |
+
* @param severity filter by severity level
|
| 42 |
+
* @param status filter by status
|
| 43 |
+
* @param limit maximum number of action items to return
|
| 44 |
+
* @return aggregated action items with SLA and business impact metrics
|
| 45 |
+
*/
|
| 46 |
+
ActionItemsDTO getActionItems(Boolean assignedToUser, String severity, String status, Integer limit);
|
| 47 |
+
|
| 48 |
+
/**
|
| 49 |
+
* Refresh all dashboard caches for performance optimization
|
| 50 |
+
* Triggers cache warming for common dashboard queries
|
| 51 |
+
*/
|
| 52 |
+
void refreshAllCaches();
|
| 53 |
+
}
|
src/main/java/com/dalab/reporting/service/INotificationService.java
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.service;
|
| 2 |
+
|
| 3 |
+
import java.util.List;
|
| 4 |
+
import java.util.UUID;
|
| 5 |
+
|
| 6 |
+
import org.springframework.data.domain.Page;
|
| 7 |
+
import org.springframework.data.domain.Pageable;
|
| 8 |
+
|
| 9 |
+
import com.dalab.reporting.dto.NotificationPreferenceDTO;
|
| 10 |
+
import com.dalab.reporting.dto.NotificationRequestDTO;
|
| 11 |
+
|
| 12 |
+
public interface INotificationService {
|
| 13 |
+
|
| 14 |
+
// Notification Preference Management
|
| 15 |
+
NotificationPreferenceDTO saveNotificationPreference(NotificationPreferenceDTO preferenceDTO);
|
| 16 |
+
List<NotificationPreferenceDTO> getNotificationPreferencesByUserId(UUID userId);
|
| 17 |
+
NotificationPreferenceDTO getNotificationPreferenceById(UUID preferenceId);
|
| 18 |
+
Page<NotificationPreferenceDTO> getAllNotificationPreferences(Pageable pageable); // Admin endpoint
|
| 19 |
+
void deleteNotificationPreference(UUID preferenceId);
|
| 20 |
+
|
| 21 |
+
// Send Notifications
|
| 22 |
+
void sendNotification(NotificationRequestDTO notificationRequest);
|
| 23 |
+
|
| 24 |
+
// Test Notification (e.g., for a user to test their setup)
|
| 25 |
+
// This might take a simplified DTO or just user ID and channel
|
| 26 |
+
void sendTestNotification(UUID userId, String channelName); // channelName like "EMAIL" or "SLACK"
|
| 27 |
+
|
| 28 |
+
}
|
src/main/java/com/dalab/reporting/service/IReportService.java
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.service;
|
| 2 |
+
|
| 3 |
+
import java.util.UUID;
|
| 4 |
+
|
| 5 |
+
import org.springframework.data.domain.Page;
|
| 6 |
+
import org.springframework.data.domain.Pageable;
|
| 7 |
+
|
| 8 |
+
import com.dalab.reporting.dto.GeneratedReportOutputDTO;
|
| 9 |
+
import com.dalab.reporting.dto.ReportDefinitionDTO;
|
| 10 |
+
import com.dalab.reporting.dto.ReportGenerationRequestDTO;
|
| 11 |
+
|
| 12 |
+
public interface IReportService {
|
| 13 |
+
|
| 14 |
+
// Report Definition Management
|
| 15 |
+
ReportDefinitionDTO createReportDefinition(ReportDefinitionDTO reportDefinitionDTO);
|
| 16 |
+
ReportDefinitionDTO getReportDefinitionById(UUID id);
|
| 17 |
+
ReportDefinitionDTO getReportDefinitionByKey(String reportKey);
|
| 18 |
+
Page<ReportDefinitionDTO> getAllReportDefinitions(Pageable pageable);
|
| 19 |
+
ReportDefinitionDTO updateReportDefinition(UUID id, ReportDefinitionDTO reportDefinitionDTO);
|
| 20 |
+
void deleteReportDefinition(UUID id);
|
| 21 |
+
|
| 22 |
+
// Report Generation & Retrieval
|
| 23 |
+
GeneratedReportOutputDTO generateReport(String reportKey, ReportGenerationRequestDTO requestDTO, UUID triggeredByUserId);
|
| 24 |
+
GeneratedReportOutputDTO generateReport(UUID reportDefinitionId, ReportGenerationRequestDTO requestDTO, UUID triggeredByUserId);
|
| 25 |
+
GeneratedReportOutputDTO regenerateReport(UUID reportId, UUID triggeredByUserId);
|
| 26 |
+
GeneratedReportOutputDTO getGeneratedReportById(UUID generatedReportId);
|
| 27 |
+
Page<GeneratedReportOutputDTO> getGeneratedReportsByReportKey(String reportKey, Pageable pageable);
|
| 28 |
+
Page<GeneratedReportOutputDTO> getAllGeneratedReports(Pageable pageable);
|
| 29 |
+
// TODO: Add methods for querying generated reports by status, user, date range, etc.
|
| 30 |
+
|
| 31 |
+
// Internal method for scheduled or event-triggered generation (implementation detail)
|
| 32 |
+
// void processReportGenerationJob(UUID generatedReportId);
|
| 33 |
+
}
|
src/main/java/com/dalab/reporting/service/impl/NotificationService.java
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.service.impl;
|
| 2 |
+
|
| 3 |
+
import java.util.Collections;
|
| 4 |
+
import java.util.List;
|
| 5 |
+
import java.util.Map;
|
| 6 |
+
import java.util.UUID;
|
| 7 |
+
import java.util.function.Function;
|
| 8 |
+
import java.util.stream.Collectors;
|
| 9 |
+
|
| 10 |
+
import org.slf4j.Logger;
|
| 11 |
+
import org.slf4j.LoggerFactory;
|
| 12 |
+
import org.springframework.data.domain.Page;
|
| 13 |
+
import org.springframework.data.domain.Pageable;
|
| 14 |
+
import org.springframework.stereotype.Service;
|
| 15 |
+
import org.springframework.transaction.annotation.Transactional;
|
| 16 |
+
import org.springframework.util.CollectionUtils;
|
| 17 |
+
|
| 18 |
+
import com.dalab.reporting.common.exception.BadRequestException;
|
| 19 |
+
import com.dalab.reporting.common.exception.ResourceNotFoundException;
|
| 20 |
+
import com.dalab.reporting.dto.NotificationPreferenceDTO;
|
| 21 |
+
import com.dalab.reporting.dto.NotificationRequestDTO;
|
| 22 |
+
import com.dalab.reporting.mapper.NotificationPreferenceMapper;
|
| 23 |
+
import com.dalab.reporting.model.NotificationChannel;
|
| 24 |
+
import com.dalab.reporting.model.UserNotificationPreference;
|
| 25 |
+
import com.dalab.reporting.repository.UserNotificationPreferenceRepository;
|
| 26 |
+
import com.dalab.reporting.service.INotificationService;
|
| 27 |
+
import com.dalab.reporting.service.notification.INotificationProvider;
|
| 28 |
+
|
| 29 |
+
@Service
|
| 30 |
+
public class NotificationService implements INotificationService {
|
| 31 |
+
|
| 32 |
+
private static final Logger log = LoggerFactory.getLogger(NotificationService.class);
|
| 33 |
+
|
| 34 |
+
private final UserNotificationPreferenceRepository preferenceRepository;
|
| 35 |
+
private final NotificationPreferenceMapper preferenceMapper;
|
| 36 |
+
private final Map<NotificationChannel, INotificationProvider> notificationProviders;
|
| 37 |
+
|
| 38 |
+
public NotificationService(
|
| 39 |
+
UserNotificationPreferenceRepository preferenceRepository,
|
| 40 |
+
NotificationPreferenceMapper preferenceMapper,
|
| 41 |
+
List<INotificationProvider> providers
|
| 42 |
+
) {
|
| 43 |
+
this.preferenceRepository = preferenceRepository;
|
| 44 |
+
this.preferenceMapper = preferenceMapper;
|
| 45 |
+
this.notificationProviders = providers.stream()
|
| 46 |
+
.collect(Collectors.toUnmodifiableMap(INotificationProvider::getChannelType, Function.identity()));
|
| 47 |
+
log.info("Initialized NotificationService with providers for channels: {}", this.notificationProviders.keySet());
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
@Override
|
| 51 |
+
@Transactional
|
| 52 |
+
public NotificationPreferenceDTO saveNotificationPreference(NotificationPreferenceDTO preferenceDTO) {
|
| 53 |
+
if (preferenceDTO.getUserId() == null || preferenceDTO.getNotificationTypeKey() == null || preferenceDTO.getChannel() == null) {
|
| 54 |
+
throw new BadRequestException("User ID, Notification Type Key, and Channel are required for preferences.");
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
UserNotificationPreference preference = preferenceRepository
|
| 58 |
+
.findByUserIdAndNotificationTypeKeyAndChannel(
|
| 59 |
+
preferenceDTO.getUserId(),
|
| 60 |
+
preferenceDTO.getNotificationTypeKey(),
|
| 61 |
+
preferenceDTO.getChannel())
|
| 62 |
+
.orElseGet(() -> preferenceMapper.toEntity(preferenceDTO));
|
| 63 |
+
|
| 64 |
+
// Update fields if it exists
|
| 65 |
+
if (preference.getId() != null) {
|
| 66 |
+
preference.setEnabled(preferenceDTO.isEnabled());
|
| 67 |
+
preference.setChannelConfiguration(preferenceDTO.getChannelConfiguration());
|
| 68 |
+
}
|
| 69 |
+
// For new preferences, mapper already set the values from DTO.
|
| 70 |
+
|
| 71 |
+
return preferenceMapper.toDTO(preferenceRepository.save(preference));
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
@Override
|
| 75 |
+
@Transactional(readOnly = true)
|
| 76 |
+
public List<NotificationPreferenceDTO> getNotificationPreferencesByUserId(UUID userId) {
|
| 77 |
+
return preferenceMapper.toDTOList(preferenceRepository.findByUserId(userId));
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
@Override
|
| 81 |
+
@Transactional(readOnly = true)
|
| 82 |
+
public NotificationPreferenceDTO getNotificationPreferenceById(UUID preferenceId) {
|
| 83 |
+
return preferenceRepository.findById(preferenceId)
|
| 84 |
+
.map(preferenceMapper::toDTO)
|
| 85 |
+
.orElseThrow(() -> new ResourceNotFoundException("NotificationPreference", "id", preferenceId));
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
@Override
|
| 89 |
+
@Transactional(readOnly = true)
|
| 90 |
+
public Page<NotificationPreferenceDTO> getAllNotificationPreferences(Pageable pageable) {
|
| 91 |
+
return preferenceRepository.findAll(pageable).map(preferenceMapper::toDTO);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
@Override
|
| 95 |
+
@Transactional
|
| 96 |
+
public void deleteNotificationPreference(UUID preferenceId) {
|
| 97 |
+
if (!preferenceRepository.existsById(preferenceId)) {
|
| 98 |
+
throw new ResourceNotFoundException("NotificationPreference", "id", preferenceId);
|
| 99 |
+
}
|
| 100 |
+
preferenceRepository.deleteById(preferenceId);
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
@Override
|
| 104 |
+
public void sendNotification(NotificationRequestDTO request) {
|
| 105 |
+
if (CollectionUtils.isEmpty(request.getChannels())) {
|
| 106 |
+
log.warn("No channels specified in notification request. Subject: {}", request.getSubject());
|
| 107 |
+
return;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
List<UserNotificationPreference> preferences = Collections.emptyList();
|
| 111 |
+
|
| 112 |
+
if (!CollectionUtils.isEmpty(request.getTargetUserIds())) {
|
| 113 |
+
// Fetch preferences for specific users and filter by requested channels
|
| 114 |
+
preferences = preferenceRepository.findAllById(request.getTargetUserIds()).stream()
|
| 115 |
+
.filter(UserNotificationPreference::isEnabled)
|
| 116 |
+
.filter(p -> request.getChannels().contains(p.getChannel()))
|
| 117 |
+
.collect(Collectors.toList());
|
| 118 |
+
// We might also need to handle cases where a user ID is given but they have no matching preference for the channel.
|
| 119 |
+
// This current logic assumes we only notify based on *existing* preferences.
|
| 120 |
+
// If direct notification to user without preference is needed, that's a different path.
|
| 121 |
+
|
| 122 |
+
} else if (request.getNotificationTypeKey() != null) {
|
| 123 |
+
// Fetch all enabled preferences for the given notification type key and requested channels
|
| 124 |
+
preferences = request.getChannels().stream()
|
| 125 |
+
.flatMap(channel ->
|
| 126 |
+
preferenceRepository.findByNotificationTypeKeyAndChannelAndEnabled(
|
| 127 |
+
request.getNotificationTypeKey(), channel, true).stream())
|
| 128 |
+
.distinct() // Avoid duplicate if user subscribed via multiple ways to same effective outcome (though our model prevents this row-wise)
|
| 129 |
+
.collect(Collectors.toList());
|
| 130 |
+
} else {
|
| 131 |
+
log.warn("Notification request must specify targetUserIds or a notificationTypeKey. Subject: {}", request.getSubject());
|
| 132 |
+
return;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
if (CollectionUtils.isEmpty(preferences)) {
|
| 136 |
+
log.info("No matching user preferences found for notification. Subject: {}", request.getSubject());
|
| 137 |
+
return;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
for (UserNotificationPreference preference : preferences) {
|
| 141 |
+
INotificationProvider provider = notificationProviders.get(preference.getChannel());
|
| 142 |
+
if (provider != null) {
|
| 143 |
+
try {
|
| 144 |
+
// Apply overrides if present
|
| 145 |
+
NotificationRequestDTO finalRequest = request;
|
| 146 |
+
if (request.getChannelOverrides() != null && request.getChannelOverrides().containsKey(preference.getChannel().name())) {
|
| 147 |
+
// This is a simplification. A real override might need deeper merging.
|
| 148 |
+
// For now, let's assume direct send is used if overrides are complex.
|
| 149 |
+
log.warn("Channel specific overrides are present but not fully implemented for preference-based send. Consider direct send. Channel: {}", preference.getChannel());
|
| 150 |
+
}
|
| 151 |
+
provider.send(preference, finalRequest);
|
| 152 |
+
log.info("Sent notification '{}' to user {} via {}", request.getSubject(), preference.getUserId(), preference.getChannel());
|
| 153 |
+
} catch (Exception e) {
|
| 154 |
+
log.error("Failed to send notification to user {} via {}. Subject: {}. Error: {}",
|
| 155 |
+
preference.getUserId(), preference.getChannel(), request.getSubject(), e.getMessage(), e);
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
@Override
|
| 162 |
+
public void sendTestNotification(UUID userId, String channelName) {
|
| 163 |
+
NotificationChannel channel;
|
| 164 |
+
try {
|
| 165 |
+
channel = NotificationChannel.valueOf(channelName.toUpperCase());
|
| 166 |
+
} catch (IllegalArgumentException e) {
|
| 167 |
+
throw new BadRequestException("Invalid notification channel: " + channelName);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
INotificationProvider provider = notificationProviders.get(channel);
|
| 171 |
+
if (provider == null) {
|
| 172 |
+
throw new BadRequestException("No provider configured for channel: " + channelName);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// For a test, we might fetch a user's primary contact for that channel or use a default test address.
|
| 176 |
+
// Here, we'll try to find a preference or send to a default if not found.
|
| 177 |
+
UserNotificationPreference testPreference = preferenceRepository
|
| 178 |
+
.findByUserIdAndNotificationTypeKeyAndChannel(userId, "TEST_NOTIFICATION", channel)
|
| 179 |
+
.orElseGet(() -> {
|
| 180 |
+
UserNotificationPreference tempPref = new UserNotificationPreference();
|
| 181 |
+
tempPref.setUserId(userId);
|
| 182 |
+
tempPref.setChannel(channel);
|
| 183 |
+
tempPref.setEnabled(true);
|
| 184 |
+
// tempPref.setChannelConfiguration(...); // Potentially set a test email/slackId if not found in profile
|
| 185 |
+
return tempPref;
|
| 186 |
+
});
|
| 187 |
+
|
| 188 |
+
if (!testPreference.isEnabled()) {
|
| 189 |
+
throw new BadRequestException("Test notifications are disabled for this user and channel through preferences.");
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
NotificationRequestDTO testRequest = NotificationRequestDTO.builder()
|
| 193 |
+
.subject("Test Notification")
|
| 194 |
+
.body("This is a test notification from DALab Reporting Service.")
|
| 195 |
+
.channels(Collections.singletonList(channel))
|
| 196 |
+
.targetUserIds(Collections.singletonList(userId)) // For context, though provider.send uses preference
|
| 197 |
+
.build();
|
| 198 |
+
|
| 199 |
+
try {
|
| 200 |
+
provider.send(testPreference, testRequest);
|
| 201 |
+
log.info("Sent test notification to user {} via {}", userId, channel);
|
| 202 |
+
} catch (Exception e) {
|
| 203 |
+
log.error("Failed to send test notification to user {} via {}: {}", userId, channel, e.getMessage(), e);
|
| 204 |
+
throw new RuntimeException("Failed to send test notification: " + e.getMessage(), e);
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
}
|
src/main/java/com/dalab/reporting/service/impl/ReportService.java
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.service.impl;
|
| 2 |
+
|
| 3 |
+
import java.time.Instant;
|
| 4 |
+
import java.util.HashMap;
|
| 5 |
+
import java.util.Map;
|
| 6 |
+
import java.util.UUID;
|
| 7 |
+
|
| 8 |
+
import org.slf4j.Logger;
|
| 9 |
+
import org.slf4j.LoggerFactory;
|
| 10 |
+
import org.springframework.data.domain.Page;
|
| 11 |
+
import org.springframework.data.domain.Pageable;
|
| 12 |
+
import org.springframework.scheduling.annotation.Async;
|
| 13 |
+
import org.springframework.stereotype.Service;
|
| 14 |
+
import org.springframework.transaction.annotation.Transactional;
|
| 15 |
+
|
| 16 |
+
import com.dalab.reporting.common.exception.BadRequestException;
|
| 17 |
+
import com.dalab.reporting.common.exception.ResourceNotFoundException;
|
| 18 |
+
import com.dalab.reporting.dto.GeneratedReportOutputDTO;
|
| 19 |
+
import com.dalab.reporting.dto.ReportDefinitionDTO;
|
| 20 |
+
import com.dalab.reporting.dto.ReportGenerationRequestDTO;
|
| 21 |
+
import com.dalab.reporting.mapper.ReportMapper;
|
| 22 |
+
import com.dalab.reporting.model.GeneratedReport;
|
| 23 |
+
import com.dalab.reporting.model.ReportDefinition;
|
| 24 |
+
import com.dalab.reporting.model.ReportStatus;
|
| 25 |
+
import com.dalab.reporting.repository.GeneratedReportRepository;
|
| 26 |
+
import com.dalab.reporting.repository.ReportDefinitionRepository;
|
| 27 |
+
import com.dalab.reporting.service.IReportService;
|
| 28 |
+
|
| 29 |
+
@Service
|
| 30 |
+
public class ReportService implements IReportService {
|
| 31 |
+
|
| 32 |
+
private static final Logger log = LoggerFactory.getLogger(ReportService.class);
|
| 33 |
+
|
| 34 |
+
private final ReportDefinitionRepository reportDefinitionRepository;
|
| 35 |
+
private final GeneratedReportRepository generatedReportRepository;
|
| 36 |
+
private final ReportMapper reportMapper;
|
| 37 |
+
// In a real app, this might be a ApplicationEventPublisher to trigger async job via Spring Events
|
| 38 |
+
// private final ApplicationEventPublisher eventPublisher;
|
| 39 |
+
|
| 40 |
+
public ReportService(ReportDefinitionRepository reportDefinitionRepository,
|
| 41 |
+
GeneratedReportRepository generatedReportRepository,
|
| 42 |
+
ReportMapper reportMapper) {
|
| 43 |
+
this.reportDefinitionRepository = reportDefinitionRepository;
|
| 44 |
+
this.generatedReportRepository = generatedReportRepository;
|
| 45 |
+
this.reportMapper = reportMapper;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
@Override
|
| 49 |
+
@Transactional
|
| 50 |
+
public ReportDefinitionDTO createReportDefinition(ReportDefinitionDTO reportDefinitionDTO) {
|
| 51 |
+
if (reportDefinitionRepository.findByReportKey(reportDefinitionDTO.getReportKey()).isPresent()) {
|
| 52 |
+
throw new BadRequestException("Report definition with key '" + reportDefinitionDTO.getReportKey() + "' already exists.");
|
| 53 |
+
}
|
| 54 |
+
ReportDefinition entity = reportMapper.toEntity(reportDefinitionDTO);
|
| 55 |
+
entity.setEnabled(true); // Default to enabled
|
| 56 |
+
return reportMapper.toDTO(reportDefinitionRepository.save(entity));
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
@Override
|
| 60 |
+
@Transactional(readOnly = true)
|
| 61 |
+
public ReportDefinitionDTO getReportDefinitionById(UUID id) {
|
| 62 |
+
return reportDefinitionRepository.findById(id)
|
| 63 |
+
.map(reportMapper::toDTO)
|
| 64 |
+
.orElseThrow(() -> new ResourceNotFoundException("ReportDefinition", "id", id));
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
@Override
|
| 68 |
+
@Transactional(readOnly = true)
|
| 69 |
+
public ReportDefinitionDTO getReportDefinitionByKey(String reportKey) {
|
| 70 |
+
return reportDefinitionRepository.findByReportKey(reportKey)
|
| 71 |
+
.map(reportMapper::toDTO)
|
| 72 |
+
.orElseThrow(() -> new ResourceNotFoundException("ReportDefinition", "reportKey", reportKey));
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
@Override
|
| 76 |
+
@Transactional(readOnly = true)
|
| 77 |
+
public Page<ReportDefinitionDTO> getAllReportDefinitions(Pageable pageable) {
|
| 78 |
+
return reportDefinitionRepository.findAll(pageable).map(reportMapper::toDTO);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
@Override
|
| 82 |
+
@Transactional
|
| 83 |
+
public ReportDefinitionDTO updateReportDefinition(UUID id, ReportDefinitionDTO reportDefinitionDTO) {
|
| 84 |
+
ReportDefinition existingDefinition = reportDefinitionRepository.findById(id)
|
| 85 |
+
.orElseThrow(() -> new ResourceNotFoundException("ReportDefinition", "id", id));
|
| 86 |
+
|
| 87 |
+
// Check if reportKey is being changed and if it conflicts
|
| 88 |
+
if (!existingDefinition.getReportKey().equals(reportDefinitionDTO.getReportKey()) &&
|
| 89 |
+
reportDefinitionRepository.findByReportKey(reportDefinitionDTO.getReportKey()).isPresent()) {
|
| 90 |
+
throw new BadRequestException("Report definition with key '" + reportDefinitionDTO.getReportKey() + "' already exists.");
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
existingDefinition.setReportKey(reportDefinitionDTO.getReportKey());
|
| 94 |
+
existingDefinition.setDisplayName(reportDefinitionDTO.getDisplayName());
|
| 95 |
+
existingDefinition.setDescription(reportDefinitionDTO.getDescription());
|
| 96 |
+
existingDefinition.setDefaultParameters(reportDefinitionDTO.getDefaultParameters());
|
| 97 |
+
existingDefinition.setDefaultScheduleCron(reportDefinitionDTO.getDefaultScheduleCron());
|
| 98 |
+
existingDefinition.setEnabled(reportDefinitionDTO.isEnabled());
|
| 99 |
+
|
| 100 |
+
return reportMapper.toDTO(reportDefinitionRepository.save(existingDefinition));
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
@Override
|
| 104 |
+
@Transactional
|
| 105 |
+
public void deleteReportDefinition(UUID id) {
|
| 106 |
+
if (!reportDefinitionRepository.existsById(id)) {
|
| 107 |
+
throw new ResourceNotFoundException("ReportDefinition", "id", id);
|
| 108 |
+
}
|
| 109 |
+
// TODO: Add logic to handle existing GeneratedReport instances if a definition is deleted.
|
| 110 |
+
// E.g., mark them as orphaned, disallow new generations, or prevent deletion if reports exist.
|
| 111 |
+
reportDefinitionRepository.deleteById(id);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
@Override
|
| 115 |
+
@Transactional
|
| 116 |
+
public GeneratedReportOutputDTO generateReport(String reportKey, ReportGenerationRequestDTO requestDTO, UUID triggeredByUserId) {
|
| 117 |
+
ReportDefinition reportDefinition = reportDefinitionRepository.findByReportKey(reportKey)
|
| 118 |
+
.orElseThrow(() -> new ResourceNotFoundException("ReportDefinition", "reportKey", reportKey));
|
| 119 |
+
return createAndQueueReportGeneration(reportDefinition, requestDTO, triggeredByUserId);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
@Override
|
| 123 |
+
@Transactional
|
| 124 |
+
public GeneratedReportOutputDTO generateReport(UUID reportDefinitionId, ReportGenerationRequestDTO requestDTO, UUID triggeredByUserId) {
|
| 125 |
+
ReportDefinition reportDefinition = reportDefinitionRepository.findById(reportDefinitionId)
|
| 126 |
+
.orElseThrow(() -> new ResourceNotFoundException("ReportDefinition", "id", reportDefinitionId));
|
| 127 |
+
return createAndQueueReportGeneration(reportDefinition, requestDTO, triggeredByUserId);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
private GeneratedReportOutputDTO createAndQueueReportGeneration(ReportDefinition reportDefinition, ReportGenerationRequestDTO requestDTO, UUID triggeredByUserId) {
|
| 131 |
+
if (!reportDefinition.isEnabled()) {
|
| 132 |
+
throw new BadRequestException("Report definition '" + reportDefinition.getReportKey() + "' is disabled.");
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
GeneratedReport generatedReport = new GeneratedReport();
|
| 136 |
+
generatedReport.setReportDefinition(reportDefinition);
|
| 137 |
+
generatedReport.setStatus(ReportStatus.PENDING);
|
| 138 |
+
generatedReport.setRequestedByUserId(triggeredByUserId);
|
| 139 |
+
generatedReport.setRequestedAt(Instant.now());
|
| 140 |
+
|
| 141 |
+
Map<String, Object> effectiveParams = new HashMap<>();
|
| 142 |
+
if (reportDefinition.getDefaultParameters() != null) {
|
| 143 |
+
effectiveParams.putAll(reportDefinition.getDefaultParameters());
|
| 144 |
+
}
|
| 145 |
+
if (requestDTO != null && requestDTO.getParameters() != null) {
|
| 146 |
+
effectiveParams.putAll(requestDTO.getParameters());
|
| 147 |
+
}
|
| 148 |
+
generatedReport.setGenerationParameters(effectiveParams);
|
| 149 |
+
|
| 150 |
+
GeneratedReport savedReport = generatedReportRepository.save(generatedReport);
|
| 151 |
+
log.info("Queued report generation for reportKey: {}, generatedReportId: {}", reportDefinition.getReportKey(), savedReport.getId());
|
| 152 |
+
|
| 153 |
+
// Trigger asynchronous processing of the report
|
| 154 |
+
// This could be publishing an event, sending a message to a queue, or calling an @Async method directly.
|
| 155 |
+
processReportGenerationJobAsync(savedReport.getId(), reportDefinition.getReportKey(), effectiveParams);
|
| 156 |
+
|
| 157 |
+
return reportMapper.toDTO(savedReport);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
@Async("taskExecutor") // Assuming a TaskExecutor bean is configured for async tasks
|
| 161 |
+
public void processReportGenerationJobAsync(UUID generatedReportId, String reportKey, Map<String, Object> parameters) {
|
| 162 |
+
log.info("Starting asynchronous processing for generatedReportId: {}, reportKey: {}", generatedReportId, reportKey);
|
| 163 |
+
GeneratedReport report = generatedReportRepository.findById(generatedReportId)
|
| 164 |
+
.orElseThrow(() -> new ResourceNotFoundException("GeneratedReport", "id", generatedReportId)); // Should not happen if just saved
|
| 165 |
+
|
| 166 |
+
report.setStatus(ReportStatus.GENERATING);
|
| 167 |
+
report.setStartedAt(Instant.now());
|
| 168 |
+
generatedReportRepository.save(report);
|
| 169 |
+
|
| 170 |
+
try {
|
| 171 |
+
// Simulate report generation logic based on reportKey
|
| 172 |
+
// In a real application, this would involve fetching data from Kafka, other services (via Feign), DB, etc.
|
| 173 |
+
// and then compiling it into the reportContent.
|
| 174 |
+
log.info("Simulating report generation for: {} with params: {}", reportKey, parameters);
|
| 175 |
+
Thread.sleep(5000); // Simulate work
|
| 176 |
+
|
| 177 |
+
String reportContent = "Report content for " + reportKey + " generated at " + Instant.now() + " with params: " + parameters.toString();
|
| 178 |
+
// For complex reports, content could be JSON, CSV, or a link to a file.
|
| 179 |
+
// Example for JSON:
|
| 180 |
+
// Map<String, Object> jsonData = new HashMap<>();
|
| 181 |
+
// jsonData.put("title", "Sample Report: " + reportKey);
|
| 182 |
+
// jsonData.put("dataPoints", List.of(1,2,3,4,5));
|
| 183 |
+
// reportContent = new ObjectMapper().writeValueAsString(jsonData);
|
| 184 |
+
|
| 185 |
+
report.setReportContent(reportContent);
|
| 186 |
+
report.setStatus(ReportStatus.COMPLETED);
|
| 187 |
+
} catch (Exception e) {
|
| 188 |
+
log.error("Error during report generation for ID {}: {}", generatedReportId, e.getMessage(), e);
|
| 189 |
+
report.setStatus(ReportStatus.FAILED);
|
| 190 |
+
report.setFailureReason(e.getMessage());
|
| 191 |
+
} finally {
|
| 192 |
+
report.setCompletedAt(Instant.now());
|
| 193 |
+
generatedReportRepository.save(report);
|
| 194 |
+
log.info("Finished processing for generatedReportId: {}. Status: {}", generatedReportId, report.getStatus());
|
| 195 |
+
// TODO: Trigger notifications if configured (e.g., on completion or failure)
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
@Override
|
| 200 |
+
@Transactional(readOnly = true)
|
| 201 |
+
public GeneratedReportOutputDTO getGeneratedReportById(UUID generatedReportId) {
|
| 202 |
+
return generatedReportRepository.findById(generatedReportId)
|
| 203 |
+
.map(reportMapper::toDTO)
|
| 204 |
+
.orElseThrow(() -> new ResourceNotFoundException("GeneratedReport", "id", generatedReportId));
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
@Override
|
| 208 |
+
@Transactional(readOnly = true)
|
| 209 |
+
public Page<GeneratedReportOutputDTO> getGeneratedReportsByReportKey(String reportKey, Pageable pageable) {
|
| 210 |
+
ReportDefinition reportDefinition = reportDefinitionRepository.findByReportKey(reportKey)
|
| 211 |
+
.orElseThrow(() -> new ResourceNotFoundException("ReportDefinition", "reportKey", reportKey));
|
| 212 |
+
return generatedReportRepository.findByReportDefinition(reportDefinition, pageable).map(reportMapper::toDTO);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
@Override
|
| 216 |
+
@Transactional(readOnly = true)
|
| 217 |
+
public Page<GeneratedReportOutputDTO> getAllGeneratedReports(Pageable pageable) {
|
| 218 |
+
return generatedReportRepository.findAll(pageable).map(reportMapper::toDTO);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
@Override
|
| 222 |
+
@Transactional
|
| 223 |
+
public GeneratedReportOutputDTO regenerateReport(UUID reportId, UUID triggeredByUserId) {
|
| 224 |
+
GeneratedReport existingReport = generatedReportRepository.findById(reportId)
|
| 225 |
+
.orElseThrow(() -> new ResourceNotFoundException("GeneratedReport", "id", reportId));
|
| 226 |
+
|
| 227 |
+
// Create a new report generation based on the existing report's definition
|
| 228 |
+
return createAndQueueReportGeneration(existingReport.getReportDefinition(), null, triggeredByUserId);
|
| 229 |
+
}
|
| 230 |
+
}
|
src/main/java/com/dalab/reporting/service/notification/IEmailNotificationProvider.java
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.service.notification;
|
| 2 |
+
|
| 3 |
+
public interface IEmailNotificationProvider extends INotificationProvider {
|
| 4 |
+
// Potentially add email-specific methods here if needed in the future
|
| 5 |
+
}
|
src/main/java/com/dalab/reporting/service/notification/INotificationProvider.java
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.service.notification;
|
| 2 |
+
|
| 3 |
+
import com.dalab.reporting.dto.NotificationRequestDTO;
|
| 4 |
+
import com.dalab.reporting.model.NotificationChannel;
|
| 5 |
+
import com.dalab.reporting.model.UserNotificationPreference;
|
| 6 |
+
|
| 7 |
+
public interface INotificationProvider {
|
| 8 |
+
NotificationChannel getChannelType();
|
| 9 |
+
void send(UserNotificationPreference preference, NotificationRequestDTO request); // Send based on user preference
|
| 10 |
+
void sendDirect(NotificationRequestDTO request, String targetAddress); // Send directly, e.g. email address or Slack channel ID
|
| 11 |
+
}
|
src/main/java/com/dalab/reporting/service/notification/ISlackNotificationProvider.java
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.reporting.service.notification;
|
| 2 |
+
|
| 3 |
+
public interface ISlackNotificationProvider extends INotificationProvider {
|
| 4 |
+
// Potentially add Slack-specific methods here if needed in the future
|
| 5 |
+
// e.g., methods to format messages using Slack's Block Kit
|
| 6 |
+
}
|