Spaces:
Runtime error
Runtime error
Ajay Yadav
commited on
Commit
·
2b88acc
1
Parent(s):
04ef45d
Initial deployment of da-autodelete-dev
Browse files- Dockerfile +27 -0
- README.md +33 -5
- build.gradle.kts +20 -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/autodelete/DaAutodeleteApplication.java +35 -0
- src/main/java/com/dalab/autodelete/controller/DeletionConfigController.java +49 -0
- src/main/java/com/dalab/autodelete/controller/DeletionTaskController.java +110 -0
- src/main/java/com/dalab/autodelete/dto/DeletionConfigDTO.java +30 -0
- src/main/java/com/dalab/autodelete/dto/DeletionTaskListResponse.java +40 -0
- src/main/java/com/dalab/autodelete/dto/DeletionTaskRequest.java +58 -0
- src/main/java/com/dalab/autodelete/dto/DeletionTaskResponse.java +23 -0
- src/main/java/com/dalab/autodelete/dto/DeletionTaskStatusDTO.java +42 -0
- src/main/java/com/dalab/autodelete/dto/TaskApprovalRequest.java +20 -0
- src/main/java/com/dalab/autodelete/exception/DeletionException.java +15 -0
- src/main/java/com/dalab/autodelete/mapper/DeletionConfigMapper.java +21 -0
- src/main/java/com/dalab/autodelete/mapper/DeletionTaskMapper.java +50 -0
- src/main/java/com/dalab/autodelete/model/DeletionConfigEntity.java +40 -0
- src/main/java/com/dalab/autodelete/model/DeletionResult.java +28 -0
- src/main/java/com/dalab/autodelete/model/DeletionStatus.java +14 -0
- src/main/java/com/dalab/autodelete/model/DeletionTask.java +78 -0
- src/main/java/com/dalab/autodelete/model/DeletionTaskEntity.java +83 -0
- src/main/java/com/dalab/autodelete/provider/GcpDeletionProvider.java +45 -0
- src/main/java/com/dalab/autodelete/provider/IGcpDeletionProvider.java +43 -0
- src/main/java/com/dalab/autodelete/repository/DeletionConfigRepository.java +11 -0
- src/main/java/com/dalab/autodelete/repository/DeletionTaskRepository.java +12 -0
- src/main/java/com/dalab/autodelete/service/IDeletionConfigService.java +18 -0
- src/main/java/com/dalab/autodelete/service/IDeletionTaskService.java +48 -0
- src/main/java/com/dalab/autodelete/service/exception/DeletionTaskNotFoundException.java +15 -0
- src/main/java/com/dalab/autodelete/service/impl/DeletionConfigServiceImpl.java +84 -0
- src/main/java/com/dalab/autodelete/service/impl/DeletionTaskServiceImpl.java +156 -0
- src/main/java/com/dalab/autodelete/service/storage/BulkDeletionResult.java +30 -0
- src/main/java/com/dalab/autodelete/service/storage/DeletionResult.java +34 -0
- src/main/java/com/dalab/autodelete/service/storage/DeletionStatus.java +14 -0
- src/main/java/com/dalab/autodelete/service/storage/DeletionType.java +26 -0
- src/main/java/com/dalab/autodelete/service/storage/DeletionVerificationResult.java +29 -0
- src/main/java/com/dalab/autodelete/service/storage/ICloudDeletionService.java +74 -0
- src/main/java/com/dalab/autodelete/service/storage/StorageReclaimEstimate.java +25 -0
- src/main/resources/application.properties +72 -0
- src/test/java/com/dalab/autodelete/controller/DeletionConfigControllerTest.java +111 -0
- src/test/java/com/dalab/autodelete/controller/DeletionTaskControllerTest.java +186 -0
- src/test/java/com/dalab/autodelete/service/impl/DeletionConfigServiceImplTest.java +119 -0
- src/test/java/com/dalab/autodelete/service/impl/DeletionTaskServiceImplTest.java +240 -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-autodelete.jar"]
|
README.md
CHANGED
|
@@ -1,10 +1,38 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: da-autodelete (dev)
|
| 3 |
+
emoji: 🔧
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 8080
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# da-autodelete - dev Environment
|
| 11 |
+
|
| 12 |
+
This is the da-autodelete 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-autodelete-dev/swagger-ui.html
|
| 25 |
+
- Health Check: https://huggingface.co/spaces/dalabsai/da-autodelete-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:40:24
|
build.gradle.kts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// da-autodelete inherits common configuration from parent build.gradle.kts
|
| 2 |
+
// This build file adds autodelete-specific dependencies
|
| 3 |
+
|
| 4 |
+
dependencies {
|
| 5 |
+
// da-protos common entities and utilities
|
| 6 |
+
implementation(project(":da-protos"))
|
| 7 |
+
|
| 8 |
+
// Cloud Storage SDKs for deletion operations
|
| 9 |
+
implementation("software.amazon.awssdk:s3:2.20.162")
|
| 10 |
+
implementation("com.azure:azure-storage-blob:12.23.0")
|
| 11 |
+
implementation("com.google.cloud:google-cloud-storage:2.28.0")
|
| 12 |
+
|
| 13 |
+
// Additional dependencies specific to da-autodelete
|
| 14 |
+
implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.1.1")
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
// Configure main application class
|
| 18 |
+
configure<org.springframework.boot.gradle.dsl.SpringBootExtension> {
|
| 19 |
+
mainClass.set("com.dalab.autodelete.DaAutodeleteApplication")
|
| 20 |
+
}
|
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-autodelete.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/autodelete/DaAutodeleteApplication.java
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete;
|
| 2 |
+
|
| 3 |
+
import io.swagger.v3.oas.models.OpenAPI;
|
| 4 |
+
import io.swagger.v3.oas.models.info.Info;
|
| 5 |
+
import io.swagger.v3.oas.models.info.License;
|
| 6 |
+
import org.springframework.beans.factory.annotation.Value;
|
| 7 |
+
import org.springframework.boot.SpringApplication;
|
| 8 |
+
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
| 9 |
+
import org.springframework.context.annotation.Bean;
|
| 10 |
+
import org.springframework.scheduling.annotation.EnableAsync;
|
| 11 |
+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
| 12 |
+
|
| 13 |
+
@SpringBootApplication
|
| 14 |
+
@EnableMethodSecurity // For @PreAuthorize annotations
|
| 15 |
+
@EnableAsync // If async operations are needed
|
| 16 |
+
// @EnableFeignClients // If this service will call other services via Feign
|
| 17 |
+
public class DaAutodeleteApplication {
|
| 18 |
+
|
| 19 |
+
public static void main(String[] args) {
|
| 20 |
+
SpringApplication.run(DaAutodeleteApplication.class, args);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
@Bean
|
| 24 |
+
public OpenAPI customOpenAPI(@Value("${spring.application.name:DALab AutoDelete Service}") String appName,
|
| 25 |
+
@Value("${spring.application.description:API for AutoDelete Service}") String appDescription,
|
| 26 |
+
@Value("${spring.application.version:0.0.1-SNAPSHOT}") String appVersion) {
|
| 27 |
+
return new OpenAPI()
|
| 28 |
+
.info(new Info()
|
| 29 |
+
.title(appName)
|
| 30 |
+
.version(appVersion)
|
| 31 |
+
.description(appDescription)
|
| 32 |
+
.termsOfService("http://swagger.io/terms/") // Placeholder
|
| 33 |
+
.license(new License().name("Apache 2.0").url("http://springdoc.org"))); // Placeholder
|
| 34 |
+
}
|
| 35 |
+
}
|
src/main/java/com/dalab/autodelete/controller/DeletionConfigController.java
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.controller;
|
| 2 |
+
|
| 3 |
+
import com.dalab.autodelete.dto.DeletionConfigDTO;
|
| 4 |
+
import com.dalab.autodelete.service.IDeletionConfigService;
|
| 5 |
+
import io.swagger.v3.oas.annotations.Operation;
|
| 6 |
+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
| 7 |
+
import io.swagger.v3.oas.annotations.tags.Tag;
|
| 8 |
+
import lombok.RequiredArgsConstructor;
|
| 9 |
+
import org.springframework.http.ResponseEntity;
|
| 10 |
+
import org.springframework.security.access.prepost.PreAuthorize;
|
| 11 |
+
import org.springframework.web.bind.annotation.*;
|
| 12 |
+
|
| 13 |
+
@RestController
|
| 14 |
+
@RequestMapping("/api/v1/deletion/config")
|
| 15 |
+
@Tag(name = "Deletion Configuration API", description = "APIs for managing auto-deletion configurations")
|
| 16 |
+
@RequiredArgsConstructor
|
| 17 |
+
public class DeletionConfigController {
|
| 18 |
+
|
| 19 |
+
private final IDeletionConfigService deletionConfigService;
|
| 20 |
+
|
| 21 |
+
@Operation(summary = "Get current deletion configuration",
|
| 22 |
+
responses = {
|
| 23 |
+
@ApiResponse(responseCode = "200", description = "Successfully retrieved configuration"),
|
| 24 |
+
@ApiResponse(responseCode = "403", description = "Forbidden if user does not have required role")
|
| 25 |
+
})
|
| 26 |
+
@GetMapping
|
| 27 |
+
@PreAuthorize("hasAnyRole('ADMIN', 'DATA_STEWARD')")
|
| 28 |
+
public ResponseEntity<DeletionConfigDTO> getDeletionConfiguration() {
|
| 29 |
+
return ResponseEntity.ok(deletionConfigService.getDeletionConfig());
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
@Operation(summary = "Update deletion configuration",
|
| 33 |
+
description = "Updates the global auto-deletion configuration. Only ADMINs can perform this.",
|
| 34 |
+
responses = {
|
| 35 |
+
@ApiResponse(responseCode = "200", description = "Configuration updated successfully"),
|
| 36 |
+
@ApiResponse(responseCode = "400", description = "Invalid configuration provided"),
|
| 37 |
+
@ApiResponse(responseCode = "403", description = "Forbidden if user is not an ADMIN")
|
| 38 |
+
})
|
| 39 |
+
@PutMapping
|
| 40 |
+
@PreAuthorize("hasRole('ADMIN')")
|
| 41 |
+
public ResponseEntity<Void> updateDeletionConfiguration(@RequestBody DeletionConfigDTO deletionConfigDTO) {
|
| 42 |
+
try {
|
| 43 |
+
deletionConfigService.updateDeletionConfig(deletionConfigDTO);
|
| 44 |
+
return ResponseEntity.ok().build();
|
| 45 |
+
} catch (IllegalArgumentException e) {
|
| 46 |
+
return ResponseEntity.badRequest().build();
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
}
|
src/main/java/com/dalab/autodelete/controller/DeletionTaskController.java
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.controller;
|
| 2 |
+
|
| 3 |
+
import com.dalab.autodelete.dto.*;
|
| 4 |
+
import com.dalab.autodelete.service.IDeletionTaskService;
|
| 5 |
+
import io.swagger.v3.oas.annotations.Operation;
|
| 6 |
+
import io.swagger.v3.oas.annotations.Parameter;
|
| 7 |
+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
| 8 |
+
import io.swagger.v3.oas.annotations.tags.Tag;
|
| 9 |
+
import jakarta.validation.Valid;
|
| 10 |
+
import lombok.RequiredArgsConstructor;
|
| 11 |
+
import org.springframework.data.domain.Pageable;
|
| 12 |
+
import org.springframework.data.web.PageableDefault;
|
| 13 |
+
import org.springframework.http.HttpStatus;
|
| 14 |
+
import org.springframework.http.ResponseEntity;
|
| 15 |
+
import org.springframework.security.access.prepost.PreAuthorize;
|
| 16 |
+
import org.springframework.web.bind.annotation.*;
|
| 17 |
+
|
| 18 |
+
@RestController
|
| 19 |
+
@RequestMapping("/api/v1/deletion/tasks")
|
| 20 |
+
@Tag(name = "Deletion Task API", description = "APIs for managing auto-deletion tasks")
|
| 21 |
+
@RequiredArgsConstructor
|
| 22 |
+
public class DeletionTaskController {
|
| 23 |
+
|
| 24 |
+
private final IDeletionTaskService deletionTaskService;
|
| 25 |
+
|
| 26 |
+
@Operation(summary = "Submit a new deletion task",
|
| 27 |
+
responses = {
|
| 28 |
+
@ApiResponse(responseCode = "202", description = "Task accepted for processing or approval"),
|
| 29 |
+
@ApiResponse(responseCode = "400", description = "Invalid request payload or validation error")
|
| 30 |
+
})
|
| 31 |
+
@PostMapping
|
| 32 |
+
@PreAuthorize("hasAnyRole('ADMIN', 'DATA_STEWARD', 'AUTOMATION_SERVICE')") // AUTOMATION_SERVICE for system-triggered tasks
|
| 33 |
+
public ResponseEntity<DeletionTaskResponse> submitDeletionTask(@Valid @RequestBody DeletionTaskRequest request) {
|
| 34 |
+
DeletionTaskResponse response = deletionTaskService.submitDeletionTask(request);
|
| 35 |
+
if ("FAILED_VALIDATION".equals(response.getStatus())) {
|
| 36 |
+
return ResponseEntity.badRequest().body(response);
|
| 37 |
+
}
|
| 38 |
+
return ResponseEntity.status(HttpStatus.ACCEPTED).body(response);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
@Operation(summary = "Get deletion task status by ID",
|
| 42 |
+
responses = {
|
| 43 |
+
@ApiResponse(responseCode = "200", description = "Successfully retrieved task status"),
|
| 44 |
+
@ApiResponse(responseCode = "404", description = "Task not found")
|
| 45 |
+
})
|
| 46 |
+
@GetMapping("/{taskId}")
|
| 47 |
+
@PreAuthorize("hasAnyRole('ADMIN', 'DATA_STEWARD', 'USER', 'AUTOMATION_SERVICE')") // USER can see their own tasks, need filtering logic in service
|
| 48 |
+
public ResponseEntity<DeletionTaskStatusDTO> getDeletionTaskStatus(@Parameter(description = "ID of the deletion task") @PathVariable String taskId) {
|
| 49 |
+
DeletionTaskStatusDTO status = deletionTaskService.getTaskStatus(taskId);
|
| 50 |
+
if (status == null) {
|
| 51 |
+
return ResponseEntity.notFound().build();
|
| 52 |
+
}
|
| 53 |
+
// TODO: Add logic to check if the authenticated user is allowed to see this task (e.g., if they triggered it)
|
| 54 |
+
return ResponseEntity.ok(status);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
@Operation(summary = "List all deletion tasks",
|
| 58 |
+
description = "Lists deletion tasks with pagination. ADMINs/DATA_STEWARDS see all, others might see a filtered list.")
|
| 59 |
+
@GetMapping
|
| 60 |
+
@PreAuthorize("hasAnyRole('ADMIN', 'DATA_STEWARD', 'USER', 'AUTOMATION_SERVICE')")
|
| 61 |
+
public ResponseEntity<DeletionTaskListResponse> listDeletionTasks(@PageableDefault(size = 20) Pageable pageable) {
|
| 62 |
+
// TODO: Service layer should handle filtering based on user role (e.g., non-admins see only their tasks)
|
| 63 |
+
DeletionTaskListResponse response = deletionTaskService.listTasks(pageable);
|
| 64 |
+
return ResponseEntity.ok(response);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
@Operation(summary = "Approve a pending deletion task",
|
| 68 |
+
description = "Only users with ADMIN or DATA_STEWARD role can approve tasks.",
|
| 69 |
+
responses = {
|
| 70 |
+
@ApiResponse(responseCode = "200", description = "Task approved successfully"),
|
| 71 |
+
@ApiResponse(responseCode = "404", description = "Task not found"),
|
| 72 |
+
@ApiResponse(responseCode = "409", description = "Task is not in a state that can be approved")
|
| 73 |
+
})
|
| 74 |
+
@PostMapping("/{taskId}/approve")
|
| 75 |
+
@PreAuthorize("hasAnyRole('ADMIN', 'DATA_STEWARD')")
|
| 76 |
+
public ResponseEntity<DeletionTaskStatusDTO> approveDeletionTask(
|
| 77 |
+
@Parameter(description = "ID of the task to approve") @PathVariable String taskId,
|
| 78 |
+
@RequestBody(required = false) TaskApprovalRequest approvalRequest) {
|
| 79 |
+
DeletionTaskStatusDTO status = deletionTaskService.approveTask(taskId, approvalRequest);
|
| 80 |
+
if (status == null) { // Should be handled by an exception in a real service normally
|
| 81 |
+
return ResponseEntity.notFound().build();
|
| 82 |
+
}
|
| 83 |
+
if (status.getErrorMessages() != null && !status.getErrorMessages().isEmpty()) {
|
| 84 |
+
return ResponseEntity.status(HttpStatus.CONFLICT).body(status);
|
| 85 |
+
}
|
| 86 |
+
return ResponseEntity.ok(status);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
@Operation(summary = "Reject a pending deletion task",
|
| 90 |
+
description = "Only users with ADMIN or DATA_STEWARD role can reject tasks.",
|
| 91 |
+
responses = {
|
| 92 |
+
@ApiResponse(responseCode = "200", description = "Task rejected successfully"),
|
| 93 |
+
@ApiResponse(responseCode = "404", description = "Task not found"),
|
| 94 |
+
@ApiResponse(responseCode = "409", description = "Task is not in a state that can be rejected")
|
| 95 |
+
})
|
| 96 |
+
@PostMapping("/{taskId}/reject")
|
| 97 |
+
@PreAuthorize("hasAnyRole('ADMIN', 'DATA_STEWARD')")
|
| 98 |
+
public ResponseEntity<DeletionTaskStatusDTO> rejectDeletionTask(
|
| 99 |
+
@Parameter(description = "ID of the task to reject") @PathVariable String taskId,
|
| 100 |
+
@RequestBody(required = false) TaskApprovalRequest rejectionRequest) {
|
| 101 |
+
DeletionTaskStatusDTO status = deletionTaskService.rejectTask(taskId, rejectionRequest);
|
| 102 |
+
if (status == null) { // Should be handled by an exception
|
| 103 |
+
return ResponseEntity.notFound().build();
|
| 104 |
+
}
|
| 105 |
+
if (status.getErrorMessages() != null && !status.getErrorMessages().isEmpty()) {
|
| 106 |
+
return ResponseEntity.status(HttpStatus.CONFLICT).body(status);
|
| 107 |
+
}
|
| 108 |
+
return ResponseEntity.ok(status);
|
| 109 |
+
}
|
| 110 |
+
}
|
src/main/java/com/dalab/autodelete/dto/DeletionConfigDTO.java
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.dto;
|
| 2 |
+
|
| 3 |
+
import lombok.Builder;
|
| 4 |
+
import lombok.Data;
|
| 5 |
+
import lombok.NoArgsConstructor;
|
| 6 |
+
import lombok.AllArgsConstructor;
|
| 7 |
+
|
| 8 |
+
import java.util.List;
|
| 9 |
+
import java.util.Map;
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* DTO for representing the auto-deletion configuration.
|
| 13 |
+
*/
|
| 14 |
+
@Data
|
| 15 |
+
@Builder
|
| 16 |
+
@NoArgsConstructor
|
| 17 |
+
@AllArgsConstructor
|
| 18 |
+
public class DeletionConfigDTO {
|
| 19 |
+
|
| 20 |
+
private Boolean enabled; // Global enable/disable for auto-deletion
|
| 21 |
+
private boolean softDeleteByDefault; // If true, performs a soft delete (marks as deleted)
|
| 22 |
+
private Long defaultSoftDeleteRetentionDays; // If soft-deleted, how long before permanent deletion (if applicable)
|
| 23 |
+
private boolean requireApprovalForHardDelete; // If manual approval is needed for permanent (hard) deletion
|
| 24 |
+
private boolean requireApprovalForSoftDelete; // If manual approval is needed for soft deletion
|
| 25 |
+
|
| 26 |
+
// Could define specific deletion strategies or grace periods per data source type or classification
|
| 27 |
+
// For example: Map<String, DeletionStrategy> deletionStrategies;
|
| 28 |
+
// DeletionStrategy could include things like: immediate, after X days, notify and wait, etc.
|
| 29 |
+
private List<String> notificationEmailsOnError; // Who to notify if deletion task fails critically
|
| 30 |
+
}
|
src/main/java/com/dalab/autodelete/dto/DeletionTaskListResponse.java
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.dto;
|
| 2 |
+
|
| 3 |
+
import lombok.Builder;
|
| 4 |
+
import lombok.Data;
|
| 5 |
+
import lombok.NoArgsConstructor;
|
| 6 |
+
import lombok.AllArgsConstructor;
|
| 7 |
+
import org.springframework.data.domain.Page;
|
| 8 |
+
|
| 9 |
+
import java.util.List;
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* DTO for a paginated list of deletion task statuses.
|
| 13 |
+
*/
|
| 14 |
+
@Data
|
| 15 |
+
@Builder
|
| 16 |
+
@NoArgsConstructor
|
| 17 |
+
@AllArgsConstructor
|
| 18 |
+
public class DeletionTaskListResponse {
|
| 19 |
+
private List<DeletionTaskStatusDTO> tasks;
|
| 20 |
+
private int pageNumber;
|
| 21 |
+
private int pageSize;
|
| 22 |
+
private long totalElements;
|
| 23 |
+
private int totalPages;
|
| 24 |
+
private boolean first;
|
| 25 |
+
private boolean last;
|
| 26 |
+
private int numberOfElements;
|
| 27 |
+
|
| 28 |
+
public static DeletionTaskListResponse fromPage(Page<DeletionTaskStatusDTO> page) {
|
| 29 |
+
return DeletionTaskListResponse.builder()
|
| 30 |
+
.tasks(page.getContent())
|
| 31 |
+
.pageNumber(page.getNumber())
|
| 32 |
+
.pageSize(page.getSize())
|
| 33 |
+
.totalElements(page.getTotalElements())
|
| 34 |
+
.totalPages(page.getTotalPages())
|
| 35 |
+
.first(page.isFirst())
|
| 36 |
+
.last(page.isLast())
|
| 37 |
+
.numberOfElements(page.getNumberOfElements())
|
| 38 |
+
.build();
|
| 39 |
+
}
|
| 40 |
+
}
|
src/main/java/com/dalab/autodelete/dto/DeletionTaskRequest.java
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.dto;
|
| 2 |
+
|
| 3 |
+
import lombok.Builder;
|
| 4 |
+
import lombok.Data;
|
| 5 |
+
import lombok.NoArgsConstructor;
|
| 6 |
+
import lombok.AllArgsConstructor;
|
| 7 |
+
|
| 8 |
+
import jakarta.validation.constraints.NotEmpty;
|
| 9 |
+
import jakarta.validation.constraints.NotNull;
|
| 10 |
+
import jakarta.validation.constraints.Size;
|
| 11 |
+
import java.util.List;
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* DTO for requesting a new deletion task.
|
| 15 |
+
*/
|
| 16 |
+
@Data
|
| 17 |
+
@Builder
|
| 18 |
+
@NoArgsConstructor
|
| 19 |
+
@AllArgsConstructor
|
| 20 |
+
public class DeletionTaskRequest {
|
| 21 |
+
|
| 22 |
+
@Size(max = 255, message = "Task name cannot exceed 255 characters.")
|
| 23 |
+
private String taskName; // Optional friendly name for the task
|
| 24 |
+
|
| 25 |
+
@NotNull(message = "Deletion scope cannot be null.")
|
| 26 |
+
private DeletionScope scope;
|
| 27 |
+
|
| 28 |
+
private String triggeredBy; // User or system that triggered the task
|
| 29 |
+
private String justification; // Reason for deletion
|
| 30 |
+
|
| 31 |
+
private DeletionConfigOverride overrideConfig; // Optional config overrides for this specific task
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* Defines the scope of assets to be deleted.
|
| 35 |
+
*/
|
| 36 |
+
@Data
|
| 37 |
+
@Builder
|
| 38 |
+
@NoArgsConstructor
|
| 39 |
+
@AllArgsConstructor
|
| 40 |
+
public static class DeletionScope {
|
| 41 |
+
@NotEmpty(message = "At least one asset ID must be provided for deletion.")
|
| 42 |
+
private List<String> assetIds; // List of asset IDs to delete
|
| 43 |
+
// Could also include criteria like tags, creation date ranges, etc.
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* Allows overriding global deletion settings for this specific task.
|
| 48 |
+
*/
|
| 49 |
+
@Data
|
| 50 |
+
@Builder
|
| 51 |
+
@NoArgsConstructor
|
| 52 |
+
@AllArgsConstructor
|
| 53 |
+
public static class DeletionConfigOverride {
|
| 54 |
+
private Boolean softDelete; // Override global soft-delete setting
|
| 55 |
+
private Long softDeleteRetentionDays; // Override retention if soft-deleting
|
| 56 |
+
private Boolean requireApproval; // Override approval requirement for this task
|
| 57 |
+
}
|
| 58 |
+
}
|
src/main/java/com/dalab/autodelete/dto/DeletionTaskResponse.java
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.dto;
|
| 2 |
+
|
| 3 |
+
import lombok.Builder;
|
| 4 |
+
import lombok.Data;
|
| 5 |
+
import lombok.NoArgsConstructor;
|
| 6 |
+
import lombok.AllArgsConstructor;
|
| 7 |
+
|
| 8 |
+
import java.time.LocalDateTime;
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* DTO representing the response after submitting a deletion task.
|
| 12 |
+
*/
|
| 13 |
+
@Data
|
| 14 |
+
@Builder
|
| 15 |
+
@NoArgsConstructor
|
| 16 |
+
@AllArgsConstructor
|
| 17 |
+
public class DeletionTaskResponse {
|
| 18 |
+
private String taskId;
|
| 19 |
+
private String taskName;
|
| 20 |
+
private String status; // e.g., PENDING_APPROVAL, SUBMITTED, FAILED_VALIDATION
|
| 21 |
+
private LocalDateTime submittedAt;
|
| 22 |
+
private String message; // Any relevant message, e.g., reason for validation failure
|
| 23 |
+
}
|
src/main/java/com/dalab/autodelete/dto/DeletionTaskStatusDTO.java
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.dto;
|
| 2 |
+
|
| 3 |
+
import lombok.Builder;
|
| 4 |
+
import lombok.Data;
|
| 5 |
+
import lombok.NoArgsConstructor;
|
| 6 |
+
import lombok.AllArgsConstructor;
|
| 7 |
+
|
| 8 |
+
import java.time.LocalDateTime;
|
| 9 |
+
import java.util.List;
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* DTO for representing the detailed status of a deletion task.
|
| 13 |
+
*/
|
| 14 |
+
@Data
|
| 15 |
+
@Builder
|
| 16 |
+
@NoArgsConstructor
|
| 17 |
+
@AllArgsConstructor
|
| 18 |
+
public class DeletionTaskStatusDTO {
|
| 19 |
+
private String taskId;
|
| 20 |
+
private String taskName;
|
| 21 |
+
private String status; // PENDING_APPROVAL, APPROVED, REJECTED, SUBMITTED, IN_PROGRESS, COMPLETED, FAILED, PARTIALLY_COMPLETED
|
| 22 |
+
private LocalDateTime submittedAt;
|
| 23 |
+
private LocalDateTime startedAt;
|
| 24 |
+
private LocalDateTime completedAt;
|
| 25 |
+
private String triggeredBy;
|
| 26 |
+
private String justification;
|
| 27 |
+
|
| 28 |
+
private DeletionTaskRequest.DeletionScope scope; // Original scope
|
| 29 |
+
private DeletionTaskRequest.DeletionConfigOverride overrideConfig; // Applied overrides
|
| 30 |
+
|
| 31 |
+
private int totalAssetsInScope;
|
| 32 |
+
private int assetsProcessedSuccessfully;
|
| 33 |
+
private int assetsFailedToProcess;
|
| 34 |
+
private int assetsPendingProcessing;
|
| 35 |
+
|
| 36 |
+
// Could add a list of individual asset statuses if needed for detailed view
|
| 37 |
+
// private List<AssetDeletionStatus> assetStatuses;
|
| 38 |
+
|
| 39 |
+
private String approvalComments;
|
| 40 |
+
private String rejectionComments;
|
| 41 |
+
private List<String> errorMessages;
|
| 42 |
+
}
|
src/main/java/com/dalab/autodelete/dto/TaskApprovalRequest.java
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.dto;
|
| 2 |
+
|
| 3 |
+
import lombok.Builder;
|
| 4 |
+
import lombok.Data;
|
| 5 |
+
import lombok.NoArgsConstructor;
|
| 6 |
+
import lombok.AllArgsConstructor;
|
| 7 |
+
|
| 8 |
+
import jakarta.validation.constraints.Size;
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* DTO for providing comments during task approval or rejection.
|
| 12 |
+
*/
|
| 13 |
+
@Data
|
| 14 |
+
@Builder
|
| 15 |
+
@NoArgsConstructor
|
| 16 |
+
@AllArgsConstructor
|
| 17 |
+
public class TaskApprovalRequest {
|
| 18 |
+
@Size(max = 1000, message = "Comments cannot exceed 1000 characters.")
|
| 19 |
+
private String comments;
|
| 20 |
+
}
|
src/main/java/com/dalab/autodelete/exception/DeletionException.java
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.exception;
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Custom exception for errors occurring during deletion operations.
|
| 5 |
+
*/
|
| 6 |
+
public class DeletionException extends Exception {
|
| 7 |
+
|
| 8 |
+
public DeletionException(String message) {
|
| 9 |
+
super(message);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
public DeletionException(String message, Throwable cause) {
|
| 13 |
+
super(message, cause);
|
| 14 |
+
}
|
| 15 |
+
}
|
src/main/java/com/dalab/autodelete/mapper/DeletionConfigMapper.java
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.mapper;
|
| 2 |
+
|
| 3 |
+
import com.dalab.autodelete.dto.DeletionConfigDTO;
|
| 4 |
+
import com.dalab.autodelete.model.DeletionConfigEntity;
|
| 5 |
+
import org.mapstruct.Mapper;
|
| 6 |
+
import org.mapstruct.MappingTarget;
|
| 7 |
+
import org.mapstruct.NullValuePropertyMappingStrategy;
|
| 8 |
+
import org.mapstruct.factory.Mappers;
|
| 9 |
+
|
| 10 |
+
@Mapper(componentModel = "spring",
|
| 11 |
+
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
| 12 |
+
public interface DeletionConfigMapper {
|
| 13 |
+
|
| 14 |
+
DeletionConfigMapper INSTANCE = Mappers.getMapper(DeletionConfigMapper.class);
|
| 15 |
+
|
| 16 |
+
DeletionConfigDTO toDto(DeletionConfigEntity entity);
|
| 17 |
+
|
| 18 |
+
DeletionConfigEntity toEntity(DeletionConfigDTO dto);
|
| 19 |
+
|
| 20 |
+
void updateEntityFromDto(DeletionConfigDTO dto, @MappingTarget DeletionConfigEntity entity);
|
| 21 |
+
}
|
src/main/java/com/dalab/autodelete/mapper/DeletionTaskMapper.java
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.mapper;
|
| 2 |
+
|
| 3 |
+
import com.dalab.autodelete.dto.DeletionTaskRequest;
|
| 4 |
+
import com.dalab.autodelete.dto.DeletionTaskResponse;
|
| 5 |
+
import com.dalab.autodelete.dto.DeletionTaskStatusDTO;
|
| 6 |
+
import com.dalab.autodelete.model.DeletionTaskEntity;
|
| 7 |
+
import org.mapstruct.Mapper;
|
| 8 |
+
import org.mapstruct.Mapping;
|
| 9 |
+
import org.mapstruct.NullValuePropertyMappingStrategy;
|
| 10 |
+
import org.mapstruct.factory.Mappers;
|
| 11 |
+
|
| 12 |
+
@Mapper(componentModel = "spring",
|
| 13 |
+
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
| 14 |
+
public interface DeletionTaskMapper {
|
| 15 |
+
|
| 16 |
+
DeletionTaskMapper INSTANCE = Mappers.getMapper(DeletionTaskMapper.class);
|
| 17 |
+
|
| 18 |
+
// From Request DTO to Entity
|
| 19 |
+
@Mapping(target = "taskId", ignore = true) // taskId is generated in service
|
| 20 |
+
@Mapping(target = "status", ignore = true) // status is set in service
|
| 21 |
+
@Mapping(target = "submittedAt", ignore = true)
|
| 22 |
+
@Mapping(target = "startedAt", ignore = true)
|
| 23 |
+
@Mapping(target = "completedAt", ignore = true)
|
| 24 |
+
@Mapping(target = "totalAssetsInScope", ignore = true)
|
| 25 |
+
@Mapping(target = "assetsProcessedSuccessfully", ignore = true)
|
| 26 |
+
@Mapping(target = "assetsFailedToProcess", ignore = true)
|
| 27 |
+
@Mapping(target = "assetsPendingProcessing", ignore = true)
|
| 28 |
+
@Mapping(target = "assetStatuses", ignore = true)
|
| 29 |
+
@Mapping(target = "errorMessages", ignore = true)
|
| 30 |
+
@Mapping(target = "approvalComments", ignore = true)
|
| 31 |
+
@Mapping(target = "rejectionComments", ignore = true)
|
| 32 |
+
DeletionTaskEntity requestToEntity(DeletionTaskRequest requestDto);
|
| 33 |
+
|
| 34 |
+
// From Entity to Response DTO (simple initial response)
|
| 35 |
+
DeletionTaskResponse entityToTaskResponse(DeletionTaskEntity entity);
|
| 36 |
+
|
| 37 |
+
// From Entity to Status DTO (detailed status)
|
| 38 |
+
DeletionTaskStatusDTO entityToStatusDTO(DeletionTaskEntity entity);
|
| 39 |
+
|
| 40 |
+
// For mapping nested DTOs to nested Entity data classes (if not automatically handled by MapStruct)
|
| 41 |
+
DeletionTaskEntity.DeletionScopeData scopeDtoToData(DeletionTaskRequest.DeletionScope scopeDto);
|
| 42 |
+
DeletionTaskRequest.DeletionScope scopeDataToDto(DeletionTaskEntity.DeletionScopeData scopeData);
|
| 43 |
+
|
| 44 |
+
DeletionTaskEntity.DeletionConfigOverrideData overrideDtoToData(DeletionTaskRequest.DeletionConfigOverride overrideDto);
|
| 45 |
+
DeletionTaskRequest.DeletionConfigOverride overrideDataToDto(DeletionTaskEntity.DeletionConfigOverrideData overrideData);
|
| 46 |
+
|
| 47 |
+
// If AssetDeletionStatusData needs explicit mapping (often MapStruct handles lists of identical beans)
|
| 48 |
+
// List<DeletionTaskEntity.AssetDeletionStatusData> assetStatusDtoListToDataList(List<AssetDeletionStatusDTO> dtoList);
|
| 49 |
+
// List<AssetDeletionStatusDTO> assetStatusDataListToDtoList(List<DeletionTaskEntity.AssetDeletionStatusData> dataList);
|
| 50 |
+
}
|
src/main/java/com/dalab/autodelete/model/DeletionConfigEntity.java
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.model;
|
| 2 |
+
|
| 3 |
+
import jakarta.persistence.*;
|
| 4 |
+
import lombok.Data;
|
| 5 |
+
import lombok.NoArgsConstructor;
|
| 6 |
+
import lombok.AllArgsConstructor;
|
| 7 |
+
// import org.hibernate.annotations.JdbcTypeCode; // Not needed if not using complex JSON types directly here
|
| 8 |
+
// import org.hibernate.type.SqlTypes;
|
| 9 |
+
|
| 10 |
+
import java.util.List;
|
| 11 |
+
|
| 12 |
+
@Entity
|
| 13 |
+
@Table(name = "dalab_deletion_config")
|
| 14 |
+
@Data
|
| 15 |
+
@NoArgsConstructor
|
| 16 |
+
@AllArgsConstructor
|
| 17 |
+
public class DeletionConfigEntity {
|
| 18 |
+
|
| 19 |
+
@Id
|
| 20 |
+
private Long id; // Use a fixed ID, e.g., 1L, as it's a global config
|
| 21 |
+
|
| 22 |
+
private boolean enabled;
|
| 23 |
+
private boolean softDeleteByDefault;
|
| 24 |
+
private Long defaultSoftDeleteRetentionDays;
|
| 25 |
+
private boolean requireApprovalForHardDelete;
|
| 26 |
+
private boolean requireApprovalForSoftDelete;
|
| 27 |
+
|
| 28 |
+
@ElementCollection(fetch = FetchType.EAGER) // Store as a collection of strings
|
| 29 |
+
@CollectionTable(name = "dalab_deletion_config_notification_emails", joinColumns = @JoinColumn(name = "config_id"))
|
| 30 |
+
@Column(name = "email")
|
| 31 |
+
private List<String> notificationEmailsOnError;
|
| 32 |
+
|
| 33 |
+
// If you add more complex configurations like per-provider or per-tag rules that are better stored as JSON,
|
| 34 |
+
// you can use @JdbcTypeCode(SqlTypes.JSON) similar to da-autoarchival's ProviderConfigData.
|
| 35 |
+
// For example:
|
| 36 |
+
// @JdbcTypeCode(SqlTypes.JSON)
|
| 37 |
+
// @Column(columnDefinition = "jsonb")
|
| 38 |
+
// private Map<String, DeletionStrategyData> deletionStrategies;
|
| 39 |
+
// public static class DeletionStrategyData { /* fields */ }
|
| 40 |
+
}
|
src/main/java/com/dalab/autodelete/model/DeletionResult.java
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.model;
|
| 2 |
+
|
| 3 |
+
import java.time.OffsetDateTime;
|
| 4 |
+
|
| 5 |
+
public class DeletionResult {
|
| 6 |
+
|
| 7 |
+
private boolean success;
|
| 8 |
+
private String message; // e.g., "Object deleted successfully" or error details
|
| 9 |
+
private OffsetDateTime completionTimestamp;
|
| 10 |
+
private long itemsDeleted; // Useful for folder deletions
|
| 11 |
+
|
| 12 |
+
public DeletionResult(boolean success, String message, long itemsDeleted) {
|
| 13 |
+
this.success = success;
|
| 14 |
+
this.message = message;
|
| 15 |
+
this.itemsDeleted = itemsDeleted;
|
| 16 |
+
this.completionTimestamp = OffsetDateTime.now();
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// Getters and Setters
|
| 20 |
+
public boolean isSuccess() { return success; }
|
| 21 |
+
public void setSuccess(boolean success) { this.success = success; }
|
| 22 |
+
public String getMessage() { return message; }
|
| 23 |
+
public void setMessage(String message) { this.message = message; }
|
| 24 |
+
public OffsetDateTime getCompletionTimestamp() { return completionTimestamp; }
|
| 25 |
+
public void setCompletionTimestamp(OffsetDateTime completionTimestamp) { this.completionTimestamp = completionTimestamp; }
|
| 26 |
+
public long getItemsDeleted() { return itemsDeleted; }
|
| 27 |
+
public void setItemsDeleted(long itemsDeleted) { this.itemsDeleted = itemsDeleted; }
|
| 28 |
+
}
|
src/main/java/com/dalab/autodelete/model/DeletionStatus.java
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.model;
|
| 2 |
+
|
| 3 |
+
public enum DeletionStatus {
|
| 4 |
+
PENDING_APPROVAL, // Task is awaiting approval before execution
|
| 5 |
+
APPROVED, // Task has been approved and is ready for processing
|
| 6 |
+
REJECTED, // Task has been rejected and will not be processed
|
| 7 |
+
SUBMITTED, // Task submitted directly (no approval needed) or post-approval, ready for queue
|
| 8 |
+
QUEUED, // Task is in the processing queue
|
| 9 |
+
IN_PROGRESS, // Task execution is actively in progress
|
| 10 |
+
COMPLETED, // Task completed successfully (all assets processed)
|
| 11 |
+
PARTIALLY_COMPLETED, // Task completed, but some assets failed to delete
|
| 12 |
+
FAILED, // Task failed entirely or due to a critical error
|
| 13 |
+
CANCELLED // Task was cancelled before completion
|
| 14 |
+
}
|
src/main/java/com/dalab/autodelete/model/DeletionTask.java
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.model;
|
| 2 |
+
|
| 3 |
+
import java.time.OffsetDateTime;
|
| 4 |
+
import java.util.Map;
|
| 5 |
+
|
| 6 |
+
public class DeletionTask {
|
| 7 |
+
|
| 8 |
+
private String taskId;
|
| 9 |
+
private String assetId; // ID of the asset in the catalog that this deletion task refers to
|
| 10 |
+
private String gcpProjectId; // GCP Project ID where the resource resides
|
| 11 |
+
private GcpResourceType resourceType; // GCS_OBJECT, GCS_FOLDER, BIGQUERY_TABLE
|
| 12 |
+
|
| 13 |
+
// For GCS
|
| 14 |
+
private String gcsBucketName;
|
| 15 |
+
private String gcsObjectPath; // Full path for object, or prefix for folder
|
| 16 |
+
|
| 17 |
+
// For BigQuery
|
| 18 |
+
private String bqDatasetId;
|
| 19 |
+
private String bqTableId;
|
| 20 |
+
|
| 21 |
+
private DeletionStatus status;
|
| 22 |
+
private OffsetDateTime lastAttemptedAt;
|
| 23 |
+
private OffsetDateTime completedAt;
|
| 24 |
+
private String failureReason;
|
| 25 |
+
|
| 26 |
+
// Configuration specific to this deletion operation, if any (e.g., force delete)
|
| 27 |
+
private Map<String, String> operationConfig;
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
public enum GcpResourceType {
|
| 31 |
+
GCS_BUCKET,
|
| 32 |
+
GCS_OBJECT,
|
| 33 |
+
GCS_FOLDER,
|
| 34 |
+
BIGQUERY_DATASET,
|
| 35 |
+
BIGQUERY_TABLE
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
public enum DeletionStatus {
|
| 39 |
+
PENDING,
|
| 40 |
+
IN_PROGRESS,
|
| 41 |
+
COMPLETED,
|
| 42 |
+
FAILED,
|
| 43 |
+
REQUIRES_APPROVAL
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// Constructors, Getters, Setters
|
| 47 |
+
public DeletionTask(String taskId, String assetId) {
|
| 48 |
+
this.taskId = taskId;
|
| 49 |
+
this.assetId = assetId;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
public String getTaskId() { return taskId; }
|
| 53 |
+
public void setTaskId(String taskId) { this.taskId = taskId; }
|
| 54 |
+
public String getAssetId() { return assetId; }
|
| 55 |
+
public void setAssetId(String assetId) { this.assetId = assetId; }
|
| 56 |
+
public String getGcpProjectId() { return gcpProjectId; }
|
| 57 |
+
public void setGcpProjectId(String gcpProjectId) { this.gcpProjectId = gcpProjectId; }
|
| 58 |
+
public GcpResourceType getResourceType() { return resourceType; }
|
| 59 |
+
public void setResourceType(GcpResourceType resourceType) { this.resourceType = resourceType; }
|
| 60 |
+
public String getGcsBucketName() { return gcsBucketName; }
|
| 61 |
+
public void setGcsBucketName(String gcsBucketName) { this.gcsBucketName = gcsBucketName; }
|
| 62 |
+
public String getGcsObjectPath() { return gcsObjectPath; }
|
| 63 |
+
public void setGcsObjectPath(String gcsObjectPath) { this.gcsObjectPath = gcsObjectPath; }
|
| 64 |
+
public String getBqDatasetId() { return bqDatasetId; }
|
| 65 |
+
public void setBqDatasetId(String bqDatasetId) { this.bqDatasetId = bqDatasetId; }
|
| 66 |
+
public String getBqTableId() { return bqTableId; }
|
| 67 |
+
public void setBqTableId(String bqTableId) { this.bqTableId = bqTableId; }
|
| 68 |
+
public DeletionStatus getStatus() { return status; }
|
| 69 |
+
public void setStatus(DeletionStatus status) { this.status = status; }
|
| 70 |
+
public Map<String, String> getOperationConfig() { return operationConfig; }
|
| 71 |
+
public void setOperationConfig(Map<String, String> operationConfig) { this.operationConfig = operationConfig; }
|
| 72 |
+
public OffsetDateTime getLastAttemptedAt() { return lastAttemptedAt; }
|
| 73 |
+
public void setLastAttemptedAt(OffsetDateTime lastAttemptedAt) { this.lastAttemptedAt = lastAttemptedAt; }
|
| 74 |
+
public OffsetDateTime getCompletedAt() { return completedAt; }
|
| 75 |
+
public void setCompletedAt(OffsetDateTime completedAt) { this.completedAt = completedAt; }
|
| 76 |
+
public String getFailureReason() { return failureReason; }
|
| 77 |
+
public void setFailureReason(String failureReason) { this.failureReason = failureReason; }
|
| 78 |
+
}
|
src/main/java/com/dalab/autodelete/model/DeletionTaskEntity.java
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.model;
|
| 2 |
+
|
| 3 |
+
import jakarta.persistence.*;
|
| 4 |
+
import lombok.Data;
|
| 5 |
+
import lombok.NoArgsConstructor;
|
| 6 |
+
import lombok.AllArgsConstructor;
|
| 7 |
+
import org.hibernate.annotations.JdbcTypeCode;
|
| 8 |
+
import org.hibernate.type.SqlTypes;
|
| 9 |
+
|
| 10 |
+
import java.time.LocalDateTime;
|
| 11 |
+
import java.util.List;
|
| 12 |
+
// import java.util.Map; // If using Map for complex JSON fields
|
| 13 |
+
|
| 14 |
+
@Entity
|
| 15 |
+
@Table(name = "dalab_deletion_tasks")
|
| 16 |
+
@Data
|
| 17 |
+
@NoArgsConstructor
|
| 18 |
+
@AllArgsConstructor
|
| 19 |
+
public class DeletionTaskEntity {
|
| 20 |
+
|
| 21 |
+
@Id
|
| 22 |
+
private String taskId;
|
| 23 |
+
|
| 24 |
+
private String taskName;
|
| 25 |
+
|
| 26 |
+
@Enumerated(EnumType.STRING)
|
| 27 |
+
private DeletionStatus status;
|
| 28 |
+
|
| 29 |
+
private LocalDateTime submittedAt;
|
| 30 |
+
private LocalDateTime startedAt;
|
| 31 |
+
private LocalDateTime completedAt;
|
| 32 |
+
|
| 33 |
+
private String triggeredBy;
|
| 34 |
+
private String justification;
|
| 35 |
+
|
| 36 |
+
@JdbcTypeCode(SqlTypes.JSON)
|
| 37 |
+
@Column(columnDefinition = "jsonb")
|
| 38 |
+
private DeletionScopeData scope; // Store as JSON
|
| 39 |
+
|
| 40 |
+
@JdbcTypeCode(SqlTypes.JSON)
|
| 41 |
+
@Column(columnDefinition = "jsonb")
|
| 42 |
+
private DeletionConfigOverrideData overrideConfig; // Store as JSON
|
| 43 |
+
|
| 44 |
+
private int totalAssetsInScope;
|
| 45 |
+
private int assetsProcessedSuccessfully;
|
| 46 |
+
private int assetsFailedToProcess;
|
| 47 |
+
private int assetsPendingProcessing;
|
| 48 |
+
|
| 49 |
+
@JdbcTypeCode(SqlTypes.JSON)
|
| 50 |
+
@Column(columnDefinition = "jsonb")
|
| 51 |
+
private List<AssetDeletionStatusData> assetStatuses; // Store detailed asset status as JSON
|
| 52 |
+
|
| 53 |
+
@JdbcTypeCode(SqlTypes.JSON)
|
| 54 |
+
@Column(columnDefinition = "jsonb")
|
| 55 |
+
private List<String> errorMessages; // Store task-level error messages as JSON
|
| 56 |
+
|
| 57 |
+
private String approvalComments;
|
| 58 |
+
private String rejectionComments;
|
| 59 |
+
|
| 60 |
+
// Static inner classes for JSONB data structures
|
| 61 |
+
@Data @NoArgsConstructor @AllArgsConstructor
|
| 62 |
+
public static class DeletionScopeData {
|
| 63 |
+
private List<String> assetIds;
|
| 64 |
+
// Potentially add other criteria here like tags, patterns etc.
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
@Data @NoArgsConstructor @AllArgsConstructor
|
| 68 |
+
public static class DeletionConfigOverrideData {
|
| 69 |
+
private Boolean softDelete;
|
| 70 |
+
private Long softDeleteRetentionDays;
|
| 71 |
+
private Boolean requireApproval;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
@Data @NoArgsConstructor @AllArgsConstructor
|
| 75 |
+
public static class AssetDeletionStatusData {
|
| 76 |
+
private String assetId;
|
| 77 |
+
private String assetName; // Optional, for easier reporting
|
| 78 |
+
private String status; // e.g., PENDING, SOFT_DELETED, HARD_DELETED, FAILED
|
| 79 |
+
private String deletionType; // SOFT, HARD
|
| 80 |
+
private String errorMessage;
|
| 81 |
+
private LocalDateTime processedAt;
|
| 82 |
+
}
|
| 83 |
+
}
|
src/main/java/com/dalab/autodelete/provider/GcpDeletionProvider.java
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.provider;
|
| 2 |
+
|
| 3 |
+
import java.util.Map;
|
| 4 |
+
|
| 5 |
+
import org.springframework.stereotype.Component;
|
| 6 |
+
|
| 7 |
+
import com.dalab.autodelete.exception.DeletionException;
|
| 8 |
+
import com.dalab.autodelete.model.DeletionResult;
|
| 9 |
+
import com.dalab.autodelete.model.DeletionTask;
|
| 10 |
+
|
| 11 |
+
@Component // Or configure as a bean
|
| 12 |
+
public class GcpDeletionProvider implements IGcpDeletionProvider {
|
| 13 |
+
|
| 14 |
+
private static final String NOT_IMPLEMENTED_MESSAGE = "GCP deletion feature is not yet implemented for this resource type.";
|
| 15 |
+
|
| 16 |
+
@Override
|
| 17 |
+
public DeletionResult deleteGcsObject(DeletionTask task, Map<String, String> providerConfig) throws DeletionException {
|
| 18 |
+
// TODO: Implement GCS object deletion
|
| 19 |
+
// 1. Get GCS client (com.google.cloud.storage.Storage)
|
| 20 |
+
// 2. Use task.getGcsBucketName(), task.getGcsObjectPath()
|
| 21 |
+
// 3. Call storage.delete(bucketName, objectPath)
|
| 22 |
+
// 4. Return DeletionResult
|
| 23 |
+
throw new DeletionException(String.format("GCS object deletion for '%s/%s' not yet implemented.", task.getGcsBucketName(), task.getGcsObjectPath()));
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
@Override
|
| 27 |
+
public DeletionResult deleteGcsFolder(DeletionTask task, Map<String, String> providerConfig) throws DeletionException {
|
| 28 |
+
// TODO: Implement GCS folder deletion (iterating and deleting objects with prefix)
|
| 29 |
+
// 1. Get GCS client
|
| 30 |
+
// 2. List objects with prefix task.getGcsObjectPath() in bucket task.getGcsBucketName()
|
| 31 |
+
// 3. Delete each object found
|
| 32 |
+
// 4. Return DeletionResult with count of items deleted
|
| 33 |
+
throw new DeletionException(String.format("GCS folder deletion for prefix '%s' in bucket '%s' not yet implemented.", task.getGcsObjectPath(), task.getGcsBucketName()));
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
@Override
|
| 37 |
+
public DeletionResult deleteBigQueryTable(DeletionTask task, Map<String, String> providerConfig) throws DeletionException {
|
| 38 |
+
// TODO: Implement BigQuery table deletion
|
| 39 |
+
// 1. Get BigQuery client (com.google.cloud.bigquery.BigQuery)
|
| 40 |
+
// 2. Use task.getGcpProjectId(), task.getBqDatasetId(), task.getBqTableId()
|
| 41 |
+
// 3. Call bigquery.delete(TableId.of(projectId, datasetId, tableId))
|
| 42 |
+
// 4. Return DeletionResult
|
| 43 |
+
throw new DeletionException(String.format("BigQuery table deletion for '%s:%s.%s' not yet implemented.", task.getGcpProjectId(), task.getBqDatasetId(), task.getBqTableId()));
|
| 44 |
+
}
|
| 45 |
+
}
|
src/main/java/com/dalab/autodelete/provider/IGcpDeletionProvider.java
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.provider;
|
| 2 |
+
|
| 3 |
+
import java.util.Map;
|
| 4 |
+
|
| 5 |
+
import com.dalab.autodelete.exception.DeletionException;
|
| 6 |
+
import com.dalab.autodelete.model.DeletionResult;
|
| 7 |
+
import com.dalab.autodelete.model.DeletionTask;
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* Interface for GCP-specific deletion operations.
|
| 11 |
+
*/
|
| 12 |
+
public interface IGcpDeletionProvider {
|
| 13 |
+
|
| 14 |
+
String PROVIDER_TYPE = "GCP"; // Constant for provider type
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Deletes a GCS object.
|
| 18 |
+
* @param task Details of the deletion task, including bucket and object path.
|
| 19 |
+
* @param providerConfig Provider-specific configurations (e.g., credentials, project if not in task).
|
| 20 |
+
* @return Result of the deletion operation.
|
| 21 |
+
* @throws DeletionException if an error occurs.
|
| 22 |
+
*/
|
| 23 |
+
DeletionResult deleteGcsObject(DeletionTask task, Map<String, String> providerConfig) throws DeletionException;
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* Deletes a GCS "folder" (objects with a common prefix).
|
| 27 |
+
* @param task Details of the deletion task, including bucket and folder prefix.
|
| 28 |
+
* @param providerConfig Provider-specific configurations.
|
| 29 |
+
* @return Result of the deletion operation, potentially including count of deleted objects.
|
| 30 |
+
* @throws DeletionException if an error occurs.
|
| 31 |
+
*/
|
| 32 |
+
DeletionResult deleteGcsFolder(DeletionTask task, Map<String, String> providerConfig) throws DeletionException;
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* Deletes a BigQuery table.
|
| 36 |
+
* @param task Details of the deletion task, including dataset and table ID.
|
| 37 |
+
* @param providerConfig Provider-specific configurations.
|
| 38 |
+
* @return Result of the deletion operation.
|
| 39 |
+
* @throws DeletionException if an error occurs.
|
| 40 |
+
*/
|
| 41 |
+
DeletionResult deleteBigQueryTable(DeletionTask task, Map<String, String> providerConfig) throws DeletionException;
|
| 42 |
+
|
| 43 |
+
}
|
src/main/java/com/dalab/autodelete/repository/DeletionConfigRepository.java
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.repository;
|
| 2 |
+
|
| 3 |
+
import com.dalab.autodelete.model.DeletionConfigEntity;
|
| 4 |
+
import org.springframework.data.jpa.repository.JpaRepository;
|
| 5 |
+
import org.springframework.stereotype.Repository;
|
| 6 |
+
|
| 7 |
+
@Repository
|
| 8 |
+
public interface DeletionConfigRepository extends JpaRepository<DeletionConfigEntity, Long> {
|
| 9 |
+
// Global config with fixed ID (1L), specific find methods likely not needed.
|
| 10 |
+
// findById(1L) will be used.
|
| 11 |
+
}
|
src/main/java/com/dalab/autodelete/repository/DeletionTaskRepository.java
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.repository;
|
| 2 |
+
|
| 3 |
+
import com.dalab.autodelete.model.DeletionTaskEntity;
|
| 4 |
+
import org.springframework.data.jpa.repository.JpaRepository;
|
| 5 |
+
import org.springframework.stereotype.Repository;
|
| 6 |
+
|
| 7 |
+
@Repository
|
| 8 |
+
public interface DeletionTaskRepository extends JpaRepository<DeletionTaskEntity, String> {
|
| 9 |
+
// Custom query methods can be added here if needed, e.g.:
|
| 10 |
+
// List<DeletionTaskEntity> findByStatus(com.dalab.autodelete.model.DeletionStatus status);
|
| 11 |
+
// Page<DeletionTaskEntity> findByTriggeredBy(String triggeredBy, org.springframework.data.domain.Pageable pageable);
|
| 12 |
+
}
|
src/main/java/com/dalab/autodelete/service/IDeletionConfigService.java
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.service;
|
| 2 |
+
|
| 3 |
+
import com.dalab.autodelete.dto.DeletionConfigDTO;
|
| 4 |
+
|
| 5 |
+
public interface IDeletionConfigService {
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Retrieves the current deletion configuration.
|
| 9 |
+
* @return The current DeletionConfigDTO.
|
| 10 |
+
*/
|
| 11 |
+
DeletionConfigDTO getDeletionConfig();
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Updates the deletion configuration.
|
| 15 |
+
* @param deletionConfigDTO The new configuration to apply.
|
| 16 |
+
*/
|
| 17 |
+
void updateDeletionConfig(DeletionConfigDTO deletionConfigDTO);
|
| 18 |
+
}
|
src/main/java/com/dalab/autodelete/service/IDeletionTaskService.java
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.service;
|
| 2 |
+
|
| 3 |
+
import com.dalab.autodelete.dto.DeletionTaskRequest;
|
| 4 |
+
import com.dalab.autodelete.dto.DeletionTaskResponse;
|
| 5 |
+
import com.dalab.autodelete.dto.DeletionTaskStatusDTO;
|
| 6 |
+
import com.dalab.autodelete.dto.DeletionTaskListResponse;
|
| 7 |
+
import com.dalab.autodelete.dto.TaskApprovalRequest;
|
| 8 |
+
import org.springframework.data.domain.Pageable;
|
| 9 |
+
|
| 10 |
+
public interface IDeletionTaskService {
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Submits a new task for asset deletion.
|
| 14 |
+
* @param request The deletion task request details.
|
| 15 |
+
* @return A response containing the task ID and initial status.
|
| 16 |
+
*/
|
| 17 |
+
DeletionTaskResponse submitDeletionTask(DeletionTaskRequest request);
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* Retrieves the status of a specific deletion task.
|
| 21 |
+
* @param taskId The ID of the task.
|
| 22 |
+
* @return The status DTO of the task, or null if not found.
|
| 23 |
+
*/
|
| 24 |
+
DeletionTaskStatusDTO getTaskStatus(String taskId);
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Lists all deletion tasks with pagination.
|
| 28 |
+
* @param pageable Pagination information.
|
| 29 |
+
* @return A paginated list of task statuses.
|
| 30 |
+
*/
|
| 31 |
+
DeletionTaskListResponse listTasks(Pageable pageable);
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* Approves a pending deletion task.
|
| 35 |
+
* @param taskId The ID of the task to approve.
|
| 36 |
+
* @param approvalRequest Details for the approval (e.g., comments).
|
| 37 |
+
* @return The updated status DTO of the task.
|
| 38 |
+
*/
|
| 39 |
+
DeletionTaskStatusDTO approveTask(String taskId, TaskApprovalRequest approvalRequest);
|
| 40 |
+
|
| 41 |
+
/**
|
| 42 |
+
* Rejects a pending deletion task.
|
| 43 |
+
* @param taskId The ID of the task to reject.
|
| 44 |
+
* @param rejectionRequest Details for the rejection (e.g., comments).
|
| 45 |
+
* @return The updated status DTO of the task.
|
| 46 |
+
*/
|
| 47 |
+
DeletionTaskStatusDTO rejectTask(String taskId, TaskApprovalRequest rejectionRequest);
|
| 48 |
+
}
|
src/main/java/com/dalab/autodelete/service/exception/DeletionTaskNotFoundException.java
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.service.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 DeletionTaskNotFoundException extends RuntimeException {
|
| 8 |
+
public DeletionTaskNotFoundException(String taskId, String operation) {
|
| 9 |
+
super(String.format("Deletion task with ID '%s' not found for operation: %s", taskId, operation));
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
public DeletionTaskNotFoundException(String taskId) {
|
| 13 |
+
super(String.format("Deletion task with ID '%s' not found.", taskId));
|
| 14 |
+
}
|
| 15 |
+
}
|
src/main/java/com/dalab/autodelete/service/impl/DeletionConfigServiceImpl.java
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.service.impl;
|
| 2 |
+
|
| 3 |
+
import com.dalab.autodelete.dto.DeletionConfigDTO;
|
| 4 |
+
import com.dalab.autodelete.mapper.DeletionConfigMapper;
|
| 5 |
+
import com.dalab.autodelete.model.DeletionConfigEntity;
|
| 6 |
+
import com.dalab.autodelete.repository.DeletionConfigRepository;
|
| 7 |
+
import com.dalab.autodelete.service.IDeletionConfigService;
|
| 8 |
+
import jakarta.annotation.PostConstruct;
|
| 9 |
+
import lombok.RequiredArgsConstructor;
|
| 10 |
+
import lombok.extern.slf4j.Slf4j;
|
| 11 |
+
import org.springframework.stereotype.Service;
|
| 12 |
+
import org.springframework.transaction.annotation.Transactional;
|
| 13 |
+
|
| 14 |
+
import java.util.ArrayList;
|
| 15 |
+
import java.util.Optional;
|
| 16 |
+
|
| 17 |
+
@Service("deletionConfigService") // Explicit bean name to distinguish from potential in-memory version
|
| 18 |
+
@RequiredArgsConstructor
|
| 19 |
+
@Slf4j
|
| 20 |
+
public class DeletionConfigServiceImpl implements IDeletionConfigService {
|
| 21 |
+
|
| 22 |
+
private final DeletionConfigRepository configRepository;
|
| 23 |
+
private final DeletionConfigMapper configMapper;
|
| 24 |
+
private static final Long GLOBAL_CONFIG_ID = 1L;
|
| 25 |
+
|
| 26 |
+
@PostConstruct
|
| 27 |
+
@Transactional
|
| 28 |
+
public void init() {
|
| 29 |
+
// Ensure a default config exists if the DB is empty for this service
|
| 30 |
+
if (!configRepository.existsById(GLOBAL_CONFIG_ID)) {
|
| 31 |
+
log.info("No global deletion configuration found. Initializing with default values.");
|
| 32 |
+
DeletionConfigEntity defaultConfig = new DeletionConfigEntity(
|
| 33 |
+
GLOBAL_CONFIG_ID,
|
| 34 |
+
false, // enabled
|
| 35 |
+
true, // softDeleteByDefault
|
| 36 |
+
30L, // defaultSoftDeleteRetentionDays
|
| 37 |
+
true, // requireApprovalForHardDelete
|
| 38 |
+
false, // requireApprovalForSoftDelete
|
| 39 |
+
new ArrayList<>() // notificationEmailsOnError
|
| 40 |
+
);
|
| 41 |
+
configRepository.save(defaultConfig);
|
| 42 |
+
log.info("Default global deletion configuration saved with ID: {}", GLOBAL_CONFIG_ID);
|
| 43 |
+
} else {
|
| 44 |
+
log.info("Global deletion configuration already exists with ID: {}", GLOBAL_CONFIG_ID);
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
@Override
|
| 49 |
+
@Transactional(readOnly = true)
|
| 50 |
+
public DeletionConfigDTO getDeletionConfig() {
|
| 51 |
+
log.debug("Fetching global deletion configuration.");
|
| 52 |
+
Optional<DeletionConfigEntity> entityOptional = configRepository.findById(GLOBAL_CONFIG_ID);
|
| 53 |
+
|
| 54 |
+
return entityOptional.map(configMapper::toDto)
|
| 55 |
+
.orElseGet(() -> {
|
| 56 |
+
log.warn("Global deletion configuration (ID: {}) not found, returning empty DTO. This might indicate an issue if init() failed or was bypassed.", GLOBAL_CONFIG_ID);
|
| 57 |
+
// Consider if throwing an exception or returning a default non-persistent DTO is better here.
|
| 58 |
+
// For now, returning an empty DTO as per previous in-memory behavior for safety, but this is unusual for JPA.
|
| 59 |
+
return new DeletionConfigDTO();
|
| 60 |
+
});
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
@Override
|
| 64 |
+
@Transactional
|
| 65 |
+
public void updateDeletionConfig(DeletionConfigDTO deletionConfigDTO) {
|
| 66 |
+
if (deletionConfigDTO == null) {
|
| 67 |
+
log.warn("Attempted to update deletion configuration with null DTO.");
|
| 68 |
+
throw new IllegalArgumentException("Deletion configuration DTO cannot be null.");
|
| 69 |
+
}
|
| 70 |
+
log.info("Updating global deletion configuration.");
|
| 71 |
+
DeletionConfigEntity entity = configRepository.findById(GLOBAL_CONFIG_ID)
|
| 72 |
+
.orElseThrow(() -> {
|
| 73 |
+
log.error("Global deletion config (ID: {}) not found for update. This is unexpected post-initialization.", GLOBAL_CONFIG_ID);
|
| 74 |
+
// This state (config not found during update) should ideally not happen if PostConstruct works.
|
| 75 |
+
return new IllegalStateException("Global deletion configuration not found for update. Initialize first.");
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
configMapper.updateEntityFromDto(deletionConfigDTO, entity);
|
| 79 |
+
// Ensure ID is not accidentally changed by the mapper if DTO were to carry it
|
| 80 |
+
entity.setId(GLOBAL_CONFIG_ID);
|
| 81 |
+
configRepository.save(entity);
|
| 82 |
+
log.info("Global deletion configuration updated successfully for ID: {}.", GLOBAL_CONFIG_ID);
|
| 83 |
+
}
|
| 84 |
+
}
|
src/main/java/com/dalab/autodelete/service/impl/DeletionTaskServiceImpl.java
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.service.impl;
|
| 2 |
+
|
| 3 |
+
import com.dalab.autodelete.dto.*;
|
| 4 |
+
import com.dalab.autodelete.mapper.DeletionTaskMapper;
|
| 5 |
+
import com.dalab.autodelete.model.DeletionStatus;
|
| 6 |
+
import com.dalab.autodelete.model.DeletionTaskEntity;
|
| 7 |
+
import com.dalab.autodelete.repository.DeletionTaskRepository;
|
| 8 |
+
import com.dalab.autodelete.service.IDeletionConfigService;
|
| 9 |
+
import com.dalab.autodelete.service.IDeletionTaskService;
|
| 10 |
+
import com.dalab.autodelete.service.exception.DeletionTaskNotFoundException;
|
| 11 |
+
import lombok.RequiredArgsConstructor;
|
| 12 |
+
import lombok.extern.slf4j.Slf4j;
|
| 13 |
+
import org.springframework.data.domain.Page;
|
| 14 |
+
import org.springframework.data.domain.PageImpl;
|
| 15 |
+
import org.springframework.data.domain.Pageable;
|
| 16 |
+
import org.springframework.stereotype.Service;
|
| 17 |
+
import org.springframework.transaction.annotation.Transactional;
|
| 18 |
+
|
| 19 |
+
import java.time.LocalDateTime;
|
| 20 |
+
import java.util.Collections;
|
| 21 |
+
import java.util.List;
|
| 22 |
+
import java.util.UUID;
|
| 23 |
+
import java.util.stream.Collectors;
|
| 24 |
+
|
| 25 |
+
@Service("deletionTaskService") // Explicit bean name
|
| 26 |
+
@RequiredArgsConstructor
|
| 27 |
+
@Slf4j
|
| 28 |
+
public class DeletionTaskServiceImpl implements IDeletionTaskService {
|
| 29 |
+
|
| 30 |
+
private final DeletionTaskRepository taskRepository;
|
| 31 |
+
private final DeletionTaskMapper taskMapper;
|
| 32 |
+
private final IDeletionConfigService deletionConfigService; // JPA version
|
| 33 |
+
|
| 34 |
+
@Override
|
| 35 |
+
@Transactional
|
| 36 |
+
public DeletionTaskResponse submitDeletionTask(DeletionTaskRequest request) {
|
| 37 |
+
log.info("Submitting new deletion task: Name='{}', TriggeredBy='{}'", request.getTaskName(), request.getTriggeredBy());
|
| 38 |
+
|
| 39 |
+
if (request.getScope() == null || request.getScope().getAssetIds() == null || request.getScope().getAssetIds().isEmpty()) {
|
| 40 |
+
log.warn("Deletion task submission failed for task '{}': Asset IDs are missing.", request.getTaskName());
|
| 41 |
+
return DeletionTaskResponse.builder()
|
| 42 |
+
.taskId(null)
|
| 43 |
+
.taskName(request.getTaskName())
|
| 44 |
+
.status(DeletionStatus.FAILED.name()) // Using enum name for status consistency
|
| 45 |
+
.submittedAt(LocalDateTime.now())
|
| 46 |
+
.message("Deletion scope must contain at least one asset ID.")
|
| 47 |
+
.build();
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
DeletionTaskEntity entity = taskMapper.requestToEntity(request);
|
| 51 |
+
entity.setTaskId(UUID.randomUUID().toString());
|
| 52 |
+
entity.setSubmittedAt(LocalDateTime.now());
|
| 53 |
+
|
| 54 |
+
DeletionConfigDTO globalConfig = deletionConfigService.getDeletionConfig();
|
| 55 |
+
boolean isSoftDelete = determineIfSoftDelete(request, globalConfig);
|
| 56 |
+
boolean requiresApproval = determineApprovalRequirement(request, globalConfig, isSoftDelete);
|
| 57 |
+
|
| 58 |
+
entity.setStatus(requiresApproval ? DeletionStatus.PENDING_APPROVAL : DeletionStatus.SUBMITTED);
|
| 59 |
+
|
| 60 |
+
// Initialize asset statuses if scope is present
|
| 61 |
+
if (request.getScope() != null && request.getScope().getAssetIds() != null) {
|
| 62 |
+
List<DeletionTaskEntity.AssetDeletionStatusData> initialAssetStatuses = request.getScope().getAssetIds().stream()
|
| 63 |
+
.map(assetId -> new DeletionTaskEntity.AssetDeletionStatusData(assetId, null, "PENDING", isSoftDelete ? "SOFT" : "HARD", null, null))
|
| 64 |
+
.collect(Collectors.toList());
|
| 65 |
+
entity.setAssetStatuses(initialAssetStatuses);
|
| 66 |
+
entity.setTotalAssetsInScope(initialAssetStatuses.size());
|
| 67 |
+
entity.setAssetsPendingProcessing(initialAssetStatuses.size());
|
| 68 |
+
entity.setAssetsFailedToProcess(0);
|
| 69 |
+
entity.setAssetsProcessedSuccessfully(0);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
DeletionTaskEntity savedEntity = taskRepository.save(entity);
|
| 73 |
+
log.info("Deletion task '{}' submitted with ID {} and status {}.", savedEntity.getTaskName(), savedEntity.getTaskId(), savedEntity.getStatus());
|
| 74 |
+
return taskMapper.entityToTaskResponse(savedEntity);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
private boolean determineIfSoftDelete(DeletionTaskRequest request, DeletionConfigDTO globalConfig) {
|
| 78 |
+
if (request.getOverrideConfig() != null && request.getOverrideConfig().getSoftDelete() != null) {
|
| 79 |
+
return request.getOverrideConfig().getSoftDelete();
|
| 80 |
+
}
|
| 81 |
+
return globalConfig.isSoftDeleteByDefault();
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
private boolean determineApprovalRequirement(DeletionTaskRequest request, DeletionConfigDTO globalConfig, boolean isSoftDelete) {
|
| 85 |
+
if (request.getOverrideConfig() != null && request.getOverrideConfig().getRequireApproval() != null) {
|
| 86 |
+
return request.getOverrideConfig().getRequireApproval();
|
| 87 |
+
}
|
| 88 |
+
return isSoftDelete ? globalConfig.isRequireApprovalForSoftDelete() : globalConfig.isRequireApprovalForHardDelete();
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
@Override
|
| 92 |
+
@Transactional(readOnly = true)
|
| 93 |
+
public DeletionTaskStatusDTO getTaskStatus(String taskId) {
|
| 94 |
+
log.debug("Fetching status for deletion task ID: {}", taskId);
|
| 95 |
+
return taskRepository.findById(taskId)
|
| 96 |
+
.map(taskMapper::entityToStatusDTO)
|
| 97 |
+
.orElse(null); // Controller handles null by returning 404
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
@Override
|
| 101 |
+
@Transactional(readOnly = true)
|
| 102 |
+
public DeletionTaskListResponse listTasks(Pageable pageable) {
|
| 103 |
+
log.debug("Listing deletion tasks with pagination: {}", pageable);
|
| 104 |
+
Page<DeletionTaskEntity> taskPage = taskRepository.findAll(pageable);
|
| 105 |
+
List<DeletionTaskStatusDTO> dtoList = taskPage.getContent().stream()
|
| 106 |
+
.map(taskMapper::entityToStatusDTO)
|
| 107 |
+
.collect(Collectors.toList());
|
| 108 |
+
PageImpl<DeletionTaskStatusDTO> dtoPage = new PageImpl<>(dtoList, pageable, taskPage.getTotalElements());
|
| 109 |
+
return DeletionTaskListResponse.fromPage(dtoPage);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
@Override
|
| 113 |
+
@Transactional
|
| 114 |
+
public DeletionTaskStatusDTO approveTask(String taskId, TaskApprovalRequest approvalRequest) {
|
| 115 |
+
log.info("Approving deletion task ID: {}", taskId);
|
| 116 |
+
DeletionTaskEntity task = taskRepository.findById(taskId)
|
| 117 |
+
.orElseThrow(() -> new DeletionTaskNotFoundException(taskId, "approve"));
|
| 118 |
+
|
| 119 |
+
if (task.getStatus() != DeletionStatus.PENDING_APPROVAL) {
|
| 120 |
+
log.warn("Cannot approve task {}: not in PENDING_APPROVAL state (current: {}).", taskId, task.getStatus());
|
| 121 |
+
DeletionTaskStatusDTO currentStatusDto = taskMapper.entityToStatusDTO(task);
|
| 122 |
+
currentStatusDto.setErrorMessages(Collections.singletonList("Task is not in a state that can be approved."));
|
| 123 |
+
return currentStatusDto;
|
| 124 |
+
}
|
| 125 |
+
task.setStatus(DeletionStatus.APPROVED); // Or SUBMITTED if it bypasses a queue and goes to processing
|
| 126 |
+
if (approvalRequest != null) {
|
| 127 |
+
task.setApprovalComments(approvalRequest.getComments());
|
| 128 |
+
}
|
| 129 |
+
// TODO: Trigger async processing of the task if approval is the gate
|
| 130 |
+
DeletionTaskEntity savedTask = taskRepository.save(task);
|
| 131 |
+
log.info("Deletion task {} approved, new status: {}.", taskId, savedTask.getStatus());
|
| 132 |
+
return taskMapper.entityToStatusDTO(savedTask);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
@Override
|
| 136 |
+
@Transactional
|
| 137 |
+
public DeletionTaskStatusDTO rejectTask(String taskId, TaskApprovalRequest rejectionRequest) {
|
| 138 |
+
log.info("Rejecting deletion task ID: {}", taskId);
|
| 139 |
+
DeletionTaskEntity task = taskRepository.findById(taskId)
|
| 140 |
+
.orElseThrow(() -> new DeletionTaskNotFoundException(taskId, "reject"));
|
| 141 |
+
|
| 142 |
+
if (task.getStatus() != DeletionStatus.PENDING_APPROVAL) {
|
| 143 |
+
log.warn("Cannot reject task {}: not in PENDING_APPROVAL state (current: {}).", taskId, task.getStatus());
|
| 144 |
+
DeletionTaskStatusDTO currentStatusDto = taskMapper.entityToStatusDTO(task);
|
| 145 |
+
currentStatusDto.setErrorMessages(Collections.singletonList("Task is not in a state that can be rejected."));
|
| 146 |
+
return currentStatusDto;
|
| 147 |
+
}
|
| 148 |
+
task.setStatus(DeletionStatus.REJECTED);
|
| 149 |
+
if (rejectionRequest != null) {
|
| 150 |
+
task.setRejectionComments(rejectionRequest.getComments());
|
| 151 |
+
}
|
| 152 |
+
DeletionTaskEntity savedTask = taskRepository.save(task);
|
| 153 |
+
log.info("Deletion task {} rejected, new status: {}.", taskId, savedTask.getStatus());
|
| 154 |
+
return taskMapper.entityToStatusDTO(savedTask);
|
| 155 |
+
}
|
| 156 |
+
}
|
src/main/java/com/dalab/autodelete/service/storage/BulkDeletionResult.java
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.service.storage;
|
| 2 |
+
|
| 3 |
+
import java.time.LocalDateTime;
|
| 4 |
+
import java.util.List;
|
| 5 |
+
|
| 6 |
+
import lombok.AllArgsConstructor;
|
| 7 |
+
import lombok.Builder;
|
| 8 |
+
import lombok.Data;
|
| 9 |
+
import lombok.NoArgsConstructor;
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Result of a bulk deletion operation.
|
| 13 |
+
*/
|
| 14 |
+
@Data
|
| 15 |
+
@Builder
|
| 16 |
+
@NoArgsConstructor
|
| 17 |
+
@AllArgsConstructor
|
| 18 |
+
public class BulkDeletionResult {
|
| 19 |
+
|
| 20 |
+
private String bulkOperationId;
|
| 21 |
+
private List<DeletionResult> individualResults;
|
| 22 |
+
private int totalItems;
|
| 23 |
+
private int successfulDeletions;
|
| 24 |
+
private int failedDeletions;
|
| 25 |
+
private DeletionStatus overallStatus;
|
| 26 |
+
private LocalDateTime initiatedAt;
|
| 27 |
+
private LocalDateTime completedAt;
|
| 28 |
+
private Double totalStorageReclaimed; // In GB
|
| 29 |
+
private Double totalCostSavings; // Monthly savings in USD
|
| 30 |
+
}
|
src/main/java/com/dalab/autodelete/service/storage/DeletionResult.java
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.service.storage;
|
| 2 |
+
|
| 3 |
+
import java.time.LocalDateTime;
|
| 4 |
+
import java.util.Map;
|
| 5 |
+
import java.util.UUID;
|
| 6 |
+
|
| 7 |
+
import lombok.AllArgsConstructor;
|
| 8 |
+
import lombok.Builder;
|
| 9 |
+
import lombok.Data;
|
| 10 |
+
import lombok.NoArgsConstructor;
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Result of a deletion operation.
|
| 14 |
+
*/
|
| 15 |
+
@Data
|
| 16 |
+
@Builder
|
| 17 |
+
@NoArgsConstructor
|
| 18 |
+
@AllArgsConstructor
|
| 19 |
+
public class DeletionResult {
|
| 20 |
+
|
| 21 |
+
private UUID assetId;
|
| 22 |
+
private String operationId;
|
| 23 |
+
private String dataLocation;
|
| 24 |
+
private DeletionType deletionType;
|
| 25 |
+
private DeletionStatus status;
|
| 26 |
+
private LocalDateTime initiatedAt;
|
| 27 |
+
private LocalDateTime completedAt;
|
| 28 |
+
private Long dataSizeBytes;
|
| 29 |
+
private Map<String, String> metadata;
|
| 30 |
+
private String errorMessage;
|
| 31 |
+
private Double storageSpaceReclaimed; // In GB
|
| 32 |
+
private Double costSavings; // Monthly savings in USD
|
| 33 |
+
private String complianceLevel; // e.g., "GDPR", "HIPAA", "SOX"
|
| 34 |
+
}
|
src/main/java/com/dalab/autodelete/service/storage/DeletionStatus.java
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.service.storage;
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Status of deletion operations.
|
| 5 |
+
*/
|
| 6 |
+
public enum DeletionStatus {
|
| 7 |
+
INITIATED,
|
| 8 |
+
IN_PROGRESS,
|
| 9 |
+
COMPLETED,
|
| 10 |
+
FAILED,
|
| 11 |
+
CANCELLED,
|
| 12 |
+
PENDING_APPROVAL,
|
| 13 |
+
VERIFIED
|
| 14 |
+
}
|
src/main/java/com/dalab/autodelete/service/storage/DeletionType.java
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.service.storage;
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Types of deletion operations available.
|
| 5 |
+
*/
|
| 6 |
+
public enum DeletionType {
|
| 7 |
+
/**
|
| 8 |
+
* Soft delete - mark as deleted but keep data recoverable for a period.
|
| 9 |
+
*/
|
| 10 |
+
SOFT,
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Hard delete - permanently remove data from storage.
|
| 14 |
+
*/
|
| 15 |
+
HARD,
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Secure overwrite - overwrite data multiple times before deletion for compliance.
|
| 19 |
+
*/
|
| 20 |
+
SECURE_OVERWRITE,
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* Immediate delete - delete data immediately without any recovery period.
|
| 24 |
+
*/
|
| 25 |
+
IMMEDIATE
|
| 26 |
+
}
|
src/main/java/com/dalab/autodelete/service/storage/DeletionVerificationResult.java
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.service.storage;
|
| 2 |
+
|
| 3 |
+
import java.time.LocalDateTime;
|
| 4 |
+
import java.util.List;
|
| 5 |
+
|
| 6 |
+
import lombok.AllArgsConstructor;
|
| 7 |
+
import lombok.Builder;
|
| 8 |
+
import lombok.Data;
|
| 9 |
+
import lombok.NoArgsConstructor;
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Result of deletion verification for compliance purposes.
|
| 13 |
+
*/
|
| 14 |
+
@Data
|
| 15 |
+
@Builder
|
| 16 |
+
@NoArgsConstructor
|
| 17 |
+
@AllArgsConstructor
|
| 18 |
+
public class DeletionVerificationResult {
|
| 19 |
+
|
| 20 |
+
private String operationId;
|
| 21 |
+
private boolean isCompletelyDeleted;
|
| 22 |
+
private boolean isCompliant;
|
| 23 |
+
private List<String> complianceStandards; // e.g., ["GDPR", "HIPAA"]
|
| 24 |
+
private LocalDateTime verificationTimestamp;
|
| 25 |
+
private String verificationMethod;
|
| 26 |
+
private List<String> remainingTraces; // Any remaining data traces found
|
| 27 |
+
private String certificateUrl; // URL to compliance certificate if available
|
| 28 |
+
private String auditTrail; // Audit trail information
|
| 29 |
+
}
|
src/main/java/com/dalab/autodelete/service/storage/ICloudDeletionService.java
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.service.storage;
|
| 2 |
+
|
| 3 |
+
import java.util.List;
|
| 4 |
+
import java.util.Map;
|
| 5 |
+
import java.util.UUID;
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Interface for cloud storage deletion operations.
|
| 9 |
+
* Abstracts different cloud providers (AWS S3, Azure Blob, GCP Cloud Storage).
|
| 10 |
+
*/
|
| 11 |
+
public interface ICloudDeletionService {
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Perform secure deletion of data from cloud storage.
|
| 15 |
+
*
|
| 16 |
+
* @param assetId The unique identifier of the asset being deleted
|
| 17 |
+
* @param dataLocation The location of the data to delete (e.g., S3 key, blob name)
|
| 18 |
+
* @param deletionType The type of deletion (SOFT, HARD, SECURE_OVERWRITE)
|
| 19 |
+
* @param metadata Additional metadata for the deletion operation
|
| 20 |
+
* @return DeletionResult containing the operation details
|
| 21 |
+
*/
|
| 22 |
+
DeletionResult deleteData(UUID assetId, String dataLocation, DeletionType deletionType, Map<String, String> metadata);
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* Perform bulk deletion of multiple data objects.
|
| 26 |
+
*
|
| 27 |
+
* @param assetIds List of asset IDs to delete
|
| 28 |
+
* @param dataLocations List of data locations corresponding to the assets
|
| 29 |
+
* @param deletionType The type of deletion to perform
|
| 30 |
+
* @param metadata Additional metadata for the bulk operation
|
| 31 |
+
* @return BulkDeletionResult containing results for all operations
|
| 32 |
+
*/
|
| 33 |
+
BulkDeletionResult bulkDeleteData(List<UUID> assetIds, List<String> dataLocations, DeletionType deletionType, Map<String, String> metadata);
|
| 34 |
+
|
| 35 |
+
/**
|
| 36 |
+
* Get the status of a deletion operation.
|
| 37 |
+
*
|
| 38 |
+
* @param operationId The ID of the deletion operation
|
| 39 |
+
* @return DeletionStatus indicating the current state
|
| 40 |
+
*/
|
| 41 |
+
DeletionStatus getDeletionStatus(String operationId);
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* Calculate storage space that would be reclaimed by deletion.
|
| 45 |
+
*
|
| 46 |
+
* @param dataLocations List of data locations to analyze
|
| 47 |
+
* @return StorageReclaimEstimate with space and cost information
|
| 48 |
+
*/
|
| 49 |
+
StorageReclaimEstimate calculateStorageReclaim(List<String> dataLocations);
|
| 50 |
+
|
| 51 |
+
/**
|
| 52 |
+
* Verify that data has been completely deleted (compliance check).
|
| 53 |
+
*
|
| 54 |
+
* @param operationId The deletion operation ID
|
| 55 |
+
* @return DeletionVerificationResult with compliance details
|
| 56 |
+
*/
|
| 57 |
+
DeletionVerificationResult verifyDeletion(String operationId);
|
| 58 |
+
|
| 59 |
+
/**
|
| 60 |
+
* Check if the deletion service supports the specified cloud provider.
|
| 61 |
+
*
|
| 62 |
+
* @param cloudProvider The cloud provider (AWS, AZURE, GCP)
|
| 63 |
+
* @return true if supported, false otherwise
|
| 64 |
+
*/
|
| 65 |
+
boolean supportsCloudProvider(String cloudProvider);
|
| 66 |
+
|
| 67 |
+
/**
|
| 68 |
+
* Get available deletion types for the cloud provider.
|
| 69 |
+
*
|
| 70 |
+
* @param cloudProvider The cloud provider
|
| 71 |
+
* @return Array of available deletion type names
|
| 72 |
+
*/
|
| 73 |
+
DeletionType[] getAvailableDeletionTypes(String cloudProvider);
|
| 74 |
+
}
|
src/main/java/com/dalab/autodelete/service/storage/StorageReclaimEstimate.java
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.service.storage;
|
| 2 |
+
|
| 3 |
+
import lombok.AllArgsConstructor;
|
| 4 |
+
import lombok.Builder;
|
| 5 |
+
import lombok.Data;
|
| 6 |
+
import lombok.NoArgsConstructor;
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Estimate of storage space and cost that would be reclaimed by deletion.
|
| 10 |
+
*/
|
| 11 |
+
@Data
|
| 12 |
+
@Builder
|
| 13 |
+
@NoArgsConstructor
|
| 14 |
+
@AllArgsConstructor
|
| 15 |
+
public class StorageReclaimEstimate {
|
| 16 |
+
|
| 17 |
+
private int totalObjects;
|
| 18 |
+
private Long totalSizeBytes;
|
| 19 |
+
private Double totalSizeGB;
|
| 20 |
+
private Double currentMonthlyCost;
|
| 21 |
+
private Double reclaimedMonthlySavings;
|
| 22 |
+
private String currency;
|
| 23 |
+
private String region;
|
| 24 |
+
private String storageClass;
|
| 25 |
+
}
|
src/main/resources/application.properties
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DALab AutoDelete Service Configuration
|
| 2 |
+
spring.application.name=da-autodelete
|
| 3 |
+
server.port=8080
|
| 4 |
+
|
| 5 |
+
# Database Configuration - da_autodelete database
|
| 6 |
+
spring.datasource.url=jdbc:postgresql://localhost:5432/da_autodelete
|
| 7 |
+
spring.datasource.username=da_autodelete_user
|
| 8 |
+
spring.datasource.password=da_autodelete_pass
|
| 9 |
+
spring.datasource.driver-class-name=org.postgresql.Driver
|
| 10 |
+
|
| 11 |
+
# JPA Configuration
|
| 12 |
+
spring.jpa.hibernate.ddl-auto=update
|
| 13 |
+
spring.jpa.show-sql=false
|
| 14 |
+
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
| 15 |
+
spring.jpa.properties.hibernate.format_sql=true
|
| 16 |
+
|
| 17 |
+
# Common entities database configuration (for da-protos entities)
|
| 18 |
+
dalab.common.datasource.url=jdbc:postgresql://localhost:5432/dalab_common
|
| 19 |
+
dalab.common.datasource.username=dalab_common_user
|
| 20 |
+
dalab.common.datasource.password=dalab_common_pass
|
| 21 |
+
|
| 22 |
+
# Kafka Configuration
|
| 23 |
+
spring.kafka.bootstrap-servers=localhost:9092
|
| 24 |
+
spring.kafka.consumer.group-id=da-autodelete-group
|
| 25 |
+
spring.kafka.consumer.auto-offset-reset=earliest
|
| 26 |
+
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
| 27 |
+
spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer
|
| 28 |
+
spring.kafka.consumer.properties.spring.json.trusted.packages=*
|
| 29 |
+
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
|
| 30 |
+
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
|
| 31 |
+
|
| 32 |
+
# Kafka Topics
|
| 33 |
+
dalab.kafka.topics.policy-actions=dalab.policies.actions
|
| 34 |
+
dalab.kafka.topics.deletion-events=dalab.deletion.events
|
| 35 |
+
|
| 36 |
+
# Security Configuration (Keycloak JWT)
|
| 37 |
+
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/dalab
|
| 38 |
+
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8180/realms/dalab/protocol/openid-connect/certs
|
| 39 |
+
|
| 40 |
+
# Cloud Storage Configuration
|
| 41 |
+
# AWS S3 Configuration
|
| 42 |
+
aws.s3.region=us-east-1
|
| 43 |
+
aws.s3.bucket.primary=dalab-primary-bucket
|
| 44 |
+
|
| 45 |
+
# Azure Blob Storage Configuration
|
| 46 |
+
azure.storage.account.name=dalabprimary
|
| 47 |
+
azure.storage.container.primary=primary-container
|
| 48 |
+
|
| 49 |
+
# Google Cloud Storage Configuration
|
| 50 |
+
gcp.storage.project.id=dalab-project
|
| 51 |
+
gcp.storage.bucket.primary=dalab-gcp-primary
|
| 52 |
+
|
| 53 |
+
# Deletion Configuration
|
| 54 |
+
deletion.safety.confirmation.required=true
|
| 55 |
+
deletion.backup.enabled=true
|
| 56 |
+
deletion.backup.retention.days=30
|
| 57 |
+
deletion.secure.overwrite.enabled=false
|
| 58 |
+
deletion.batch.size=50
|
| 59 |
+
|
| 60 |
+
# Actuator Configuration
|
| 61 |
+
management.endpoints.web.exposure.include=health,info,metrics,prometheus
|
| 62 |
+
management.endpoint.health.show-details=when-authorized
|
| 63 |
+
management.metrics.export.prometheus.enabled=true
|
| 64 |
+
|
| 65 |
+
# OpenAPI Documentation
|
| 66 |
+
springdoc.api-docs.path=/v3/api-docs
|
| 67 |
+
springdoc.swagger-ui.path=/swagger-ui.html
|
| 68 |
+
|
| 69 |
+
# Logging Configuration
|
| 70 |
+
logging.level.com.dalab.autodelete=INFO
|
| 71 |
+
logging.level.org.springframework.kafka=WARN
|
| 72 |
+
logging.level.org.springframework.security=WARN
|
src/test/java/com/dalab/autodelete/controller/DeletionConfigControllerTest.java
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.controller;
|
| 2 |
+
|
| 3 |
+
import com.dalab.autodelete.dto.DeletionConfigDTO;
|
| 4 |
+
import com.dalab.autodelete.service.IDeletionConfigService;
|
| 5 |
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
| 6 |
+
import org.junit.jupiter.api.BeforeEach;
|
| 7 |
+
import org.junit.jupiter.api.Test;
|
| 8 |
+
import org.springframework.beans.factory.annotation.Autowired;
|
| 9 |
+
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
| 10 |
+
import org.springframework.boot.test.mock.mockito.MockBean;
|
| 11 |
+
import org.springframework.http.MediaType;
|
| 12 |
+
import org.springframework.security.test.context.support.WithMockUser;
|
| 13 |
+
import org.springframework.test.web.servlet.MockMvc;
|
| 14 |
+
|
| 15 |
+
import java.util.ArrayList;
|
| 16 |
+
|
| 17 |
+
import static org.mockito.ArgumentMatchers.any;
|
| 18 |
+
import static org.mockito.Mockito.*;
|
| 19 |
+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
| 20 |
+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
| 21 |
+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
| 22 |
+
|
| 23 |
+
@WebMvcTest(DeletionConfigController.class)
|
| 24 |
+
class DeletionConfigControllerTest {
|
| 25 |
+
|
| 26 |
+
@Autowired
|
| 27 |
+
private MockMvc mockMvc;
|
| 28 |
+
|
| 29 |
+
@MockBean
|
| 30 |
+
private IDeletionConfigService deletionConfigService;
|
| 31 |
+
|
| 32 |
+
@Autowired
|
| 33 |
+
private ObjectMapper objectMapper;
|
| 34 |
+
|
| 35 |
+
private DeletionConfigDTO sampleConfigDTO;
|
| 36 |
+
|
| 37 |
+
@BeforeEach
|
| 38 |
+
void setUp() {
|
| 39 |
+
sampleConfigDTO = DeletionConfigDTO.builder()
|
| 40 |
+
.enabled(true)
|
| 41 |
+
.softDeleteByDefault(true)
|
| 42 |
+
.defaultSoftDeleteRetentionDays(30L)
|
| 43 |
+
.requireApprovalForHardDelete(true)
|
| 44 |
+
.requireApprovalForSoftDelete(false)
|
| 45 |
+
.notificationEmailsOnError(new ArrayList<>())
|
| 46 |
+
.build();
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
@Test
|
| 50 |
+
@WithMockUser(authorities = "ROLE_ADMIN")
|
| 51 |
+
void getDeletionConfiguration_AsAdmin_ShouldReturnConfig() throws Exception {
|
| 52 |
+
when(deletionConfigService.getDeletionConfig()).thenReturn(sampleConfigDTO);
|
| 53 |
+
|
| 54 |
+
mockMvc.perform(get("/api/v1/deletion/config"))
|
| 55 |
+
.andExpect(status().isOk())
|
| 56 |
+
.andExpect(jsonPath("$.enabled").value(true))
|
| 57 |
+
.andExpect(jsonPath("$.softDeleteByDefault").value(true));
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
@Test
|
| 61 |
+
@WithMockUser(authorities = "ROLE_DATA_STEWARD")
|
| 62 |
+
void getDeletionConfiguration_AsDataSteward_ShouldReturnConfig() throws Exception {
|
| 63 |
+
when(deletionConfigService.getDeletionConfig()).thenReturn(sampleConfigDTO);
|
| 64 |
+
|
| 65 |
+
mockMvc.perform(get("/api/v1/deletion/config"))
|
| 66 |
+
.andExpect(status().isOk());
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
@Test
|
| 70 |
+
@WithMockUser(authorities = "ROLE_USER") // A regular user should not access this
|
| 71 |
+
void getDeletionConfiguration_AsUser_ShouldBeForbidden() throws Exception {
|
| 72 |
+
mockMvc.perform(get("/api/v1/deletion/config"))
|
| 73 |
+
.andExpect(status().isForbidden());
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
@Test
|
| 77 |
+
@WithMockUser(authorities = "ROLE_ADMIN")
|
| 78 |
+
void updateDeletionConfiguration_AsAdmin_ShouldReturnOk() throws Exception {
|
| 79 |
+
doNothing().when(deletionConfigService).updateDeletionConfig(any(DeletionConfigDTO.class));
|
| 80 |
+
|
| 81 |
+
mockMvc.perform(put("/api/v1/deletion/config")
|
| 82 |
+
.with(csrf())
|
| 83 |
+
.contentType(MediaType.APPLICATION_JSON)
|
| 84 |
+
.content(objectMapper.writeValueAsString(sampleConfigDTO)))
|
| 85 |
+
.andExpect(status().isOk());
|
| 86 |
+
|
| 87 |
+
verify(deletionConfigService, times(1)).updateDeletionConfig(any(DeletionConfigDTO.class));
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
@Test
|
| 91 |
+
@WithMockUser(authorities = "ROLE_DATA_STEWARD")
|
| 92 |
+
void updateDeletionConfiguration_AsDataSteward_ShouldBeForbidden() throws Exception {
|
| 93 |
+
mockMvc.perform(put("/api/v1/deletion/config")
|
| 94 |
+
.with(csrf())
|
| 95 |
+
.contentType(MediaType.APPLICATION_JSON)
|
| 96 |
+
.content(objectMapper.writeValueAsString(sampleConfigDTO)))
|
| 97 |
+
.andExpect(status().isForbidden());
|
| 98 |
+
}
|
| 99 |
+
@Test
|
| 100 |
+
@WithMockUser(authorities = "ROLE_ADMIN")
|
| 101 |
+
void updateDeletionConfiguration_WithNullBody_AsAdmin_ShouldReturnBadRequest() throws Exception {
|
| 102 |
+
// Simulate service throwing IllegalArgumentException for null DTO
|
| 103 |
+
doThrow(new IllegalArgumentException("DTO cannot be null")).when(deletionConfigService).updateDeletionConfig(null);
|
| 104 |
+
|
| 105 |
+
mockMvc.perform(put("/api/v1/deletion/config")
|
| 106 |
+
.with(csrf())
|
| 107 |
+
.contentType(MediaType.APPLICATION_JSON)
|
| 108 |
+
/* .content(objectMapper.writeValueAsString(null)) */) // Sending empty content or invalid content
|
| 109 |
+
.andExpect(status().isBadRequest()); // Spring typically handles invalid JSON or missing body as 400
|
| 110 |
+
}
|
| 111 |
+
}
|
src/test/java/com/dalab/autodelete/controller/DeletionTaskControllerTest.java
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.controller;
|
| 2 |
+
|
| 3 |
+
import com.dalab.autodelete.dto.*;
|
| 4 |
+
import com.dalab.autodelete.service.IDeletionTaskService;
|
| 5 |
+
import com.dalab.autodelete.service.exception.DeletionTaskNotFoundException;
|
| 6 |
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
| 7 |
+
import org.junit.jupiter.api.BeforeEach;
|
| 8 |
+
import org.junit.jupiter.api.Test;
|
| 9 |
+
import org.springframework.beans.factory.annotation.Autowired;
|
| 10 |
+
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
| 11 |
+
import org.springframework.boot.test.mock.mockito.MockBean;
|
| 12 |
+
import org.springframework.data.domain.PageImpl;
|
| 13 |
+
import org.springframework.data.domain.PageRequest;
|
| 14 |
+
import org.springframework.data.domain.Pageable;
|
| 15 |
+
import org.springframework.http.MediaType;
|
| 16 |
+
import org.springframework.security.test.context.support.WithMockUser;
|
| 17 |
+
import org.springframework.test.web.servlet.MockMvc;
|
| 18 |
+
|
| 19 |
+
import java.time.LocalDateTime;
|
| 20 |
+
import java.util.Collections;
|
| 21 |
+
import java.util.List;
|
| 22 |
+
import java.util.UUID;
|
| 23 |
+
|
| 24 |
+
import static org.mockito.ArgumentMatchers.any;
|
| 25 |
+
import static org.mockito.ArgumentMatchers.eq;
|
| 26 |
+
import static org.mockito.Mockito.*;
|
| 27 |
+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
| 28 |
+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
| 29 |
+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
| 30 |
+
|
| 31 |
+
@WebMvcTest(DeletionTaskController.class)
|
| 32 |
+
class DeletionTaskControllerTest {
|
| 33 |
+
|
| 34 |
+
@Autowired
|
| 35 |
+
private MockMvc mockMvc;
|
| 36 |
+
|
| 37 |
+
@MockBean
|
| 38 |
+
private IDeletionTaskService taskService;
|
| 39 |
+
|
| 40 |
+
@Autowired
|
| 41 |
+
private ObjectMapper objectMapper;
|
| 42 |
+
|
| 43 |
+
private DeletionTaskRequest sampleTaskRequest;
|
| 44 |
+
private DeletionTaskResponse sampleTaskResponse;
|
| 45 |
+
private DeletionTaskStatusDTO sampleTaskStatusDTO;
|
| 46 |
+
private String sampleTaskId;
|
| 47 |
+
|
| 48 |
+
@BeforeEach
|
| 49 |
+
void setUp() {
|
| 50 |
+
sampleTaskId = UUID.randomUUID().toString();
|
| 51 |
+
|
| 52 |
+
DeletionTaskRequest.DeletionScope scope = DeletionTaskRequest.DeletionScope.builder()
|
| 53 |
+
.assetIds(List.of("asset1", "asset2"))
|
| 54 |
+
.build();
|
| 55 |
+
sampleTaskRequest = DeletionTaskRequest.builder()
|
| 56 |
+
.taskName("Test Deletion Task")
|
| 57 |
+
.scope(scope)
|
| 58 |
+
.build();
|
| 59 |
+
|
| 60 |
+
sampleTaskResponse = DeletionTaskResponse.builder()
|
| 61 |
+
.taskId(sampleTaskId)
|
| 62 |
+
.taskName("Test Deletion Task")
|
| 63 |
+
.status("SUBMITTED")
|
| 64 |
+
.submittedAt(LocalDateTime.now())
|
| 65 |
+
.build();
|
| 66 |
+
|
| 67 |
+
sampleTaskStatusDTO = DeletionTaskStatusDTO.builder()
|
| 68 |
+
.taskId(sampleTaskId)
|
| 69 |
+
.taskName("Test Deletion Task")
|
| 70 |
+
.status("SUBMITTED")
|
| 71 |
+
.submittedAt(LocalDateTime.now())
|
| 72 |
+
.scope(scope) // Assuming DeletionTaskRequest.DeletionScope is compatible or mapped
|
| 73 |
+
.totalAssetsInScope(2)
|
| 74 |
+
.build();
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
@Test
|
| 78 |
+
@WithMockUser(authorities = "ROLE_ADMIN")
|
| 79 |
+
void submitDeletionTask_AsAdmin_ValidRequest_ShouldReturnAccepted() throws Exception {
|
| 80 |
+
when(taskService.submitDeletionTask(any(DeletionTaskRequest.class))).thenReturn(sampleTaskResponse);
|
| 81 |
+
|
| 82 |
+
mockMvc.perform(post("/api/v1/deletion/tasks")
|
| 83 |
+
.with(csrf())
|
| 84 |
+
.contentType(MediaType.APPLICATION_JSON)
|
| 85 |
+
.content(objectMapper.writeValueAsString(sampleTaskRequest)))
|
| 86 |
+
.andExpect(status().isAccepted())
|
| 87 |
+
.andExpect(jsonPath("$.taskId").value(sampleTaskId))
|
| 88 |
+
.andExpect(jsonPath("$.status").value("SUBMITTED"));
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
@Test
|
| 92 |
+
@WithMockUser(authorities = "ROLE_ADMIN")
|
| 93 |
+
void submitDeletionTask_InvalidScope_ShouldReturnBadRequest() throws Exception {
|
| 94 |
+
DeletionTaskRequest invalidRequest = DeletionTaskRequest.builder().taskName("Invalid Task").scope(null).build();
|
| 95 |
+
// Service returns a specific response for validation failure
|
| 96 |
+
DeletionTaskResponse validationFailedResponse = DeletionTaskResponse.builder()
|
| 97 |
+
.taskId(null)
|
| 98 |
+
.taskName(invalidRequest.getTaskName())
|
| 99 |
+
.status("FAILED_VALIDATION")
|
| 100 |
+
.message("Scope is null or empty")
|
| 101 |
+
.submittedAt(LocalDateTime.now())
|
| 102 |
+
.build();
|
| 103 |
+
when(taskService.submitDeletionTask(any(DeletionTaskRequest.class))).thenReturn(validationFailedResponse);
|
| 104 |
+
|
| 105 |
+
mockMvc.perform(post("/api/v1/deletion/tasks")
|
| 106 |
+
.with(csrf())
|
| 107 |
+
.contentType(MediaType.APPLICATION_JSON)
|
| 108 |
+
.content(objectMapper.writeValueAsString(invalidRequest)))
|
| 109 |
+
.andExpect(status().isBadRequest()) // Controller logic changes status for FAILED_VALIDATION
|
| 110 |
+
.andExpect(jsonPath("$.status").value("FAILED_VALIDATION"));
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
@Test
|
| 114 |
+
@WithMockUser(authorities = "ROLE_USER") // Assuming USER can get status of their own tasks (logic in service)
|
| 115 |
+
void getDeletionTaskStatus_WhenTaskExists_ShouldReturnStatus() throws Exception {
|
| 116 |
+
when(taskService.getTaskStatus(sampleTaskId)).thenReturn(sampleTaskStatusDTO);
|
| 117 |
+
|
| 118 |
+
mockMvc.perform(get("/api/v1/deletion/tasks/{taskId}", sampleTaskId))
|
| 119 |
+
.andExpect(status().isOk())
|
| 120 |
+
.andExpect(jsonPath("$.taskId").value(sampleTaskId))
|
| 121 |
+
.andExpect(jsonPath("$.status").value("SUBMITTED"));
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
@Test
|
| 125 |
+
@WithMockUser(authorities = "ROLE_USER")
|
| 126 |
+
void getDeletionTaskStatus_WhenTaskNotExists_ShouldReturnNotFound() throws Exception {
|
| 127 |
+
when(taskService.getTaskStatus(sampleTaskId)).thenReturn(null); // Service returns null for not found
|
| 128 |
+
|
| 129 |
+
mockMvc.perform(get("/api/v1/deletion/tasks/{taskId}", sampleTaskId))
|
| 130 |
+
.andExpect(status().isNotFound());
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
@Test
|
| 134 |
+
@WithMockUser(authorities = "ROLE_DATA_STEWARD")
|
| 135 |
+
void listDeletionTasks_ShouldReturnPageOfTasks() throws Exception {
|
| 136 |
+
List<DeletionTaskStatusDTO> taskList = Collections.singletonList(sampleTaskStatusDTO);
|
| 137 |
+
PageImpl<DeletionTaskStatusDTO> page = new PageImpl<>(taskList, PageRequest.of(0, 20), 1);
|
| 138 |
+
DeletionTaskListResponse listResponse = DeletionTaskListResponse.fromPage(page);
|
| 139 |
+
|
| 140 |
+
when(taskService.listTasks(any(Pageable.class))).thenReturn(listResponse);
|
| 141 |
+
|
| 142 |
+
mockMvc.perform(get("/api/v1/deletion/tasks").param("page", "0").param("size", "20"))
|
| 143 |
+
.andExpect(status().isOk())
|
| 144 |
+
.andExpect(jsonPath("$.tasks[0].taskId").value(sampleTaskId));
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
@Test
|
| 148 |
+
@WithMockUser(authorities = "ROLE_ADMIN")
|
| 149 |
+
void approveDeletionTask_AsAdmin_WhenTaskApprovable_ShouldReturnOk() throws Exception {
|
| 150 |
+
DeletionTaskStatusDTO approvedStatus = DeletionTaskStatusDTO.builder().taskId(sampleTaskId).status("APPROVED").build();
|
| 151 |
+
when(taskService.approveTask(eq(sampleTaskId), any())).thenReturn(approvedStatus);
|
| 152 |
+
|
| 153 |
+
mockMvc.perform(post("/api/v1/deletion/tasks/{taskId}/approve", sampleTaskId)
|
| 154 |
+
.with(csrf()))
|
| 155 |
+
.andExpect(status().isOk())
|
| 156 |
+
.andExpect(jsonPath("$.status").value("APPROVED"));
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
@Test
|
| 160 |
+
@WithMockUser(authorities = "ROLE_ADMIN")
|
| 161 |
+
void approveDeletionTask_TaskNotApprovable_ShouldReturnConflict() throws Exception {
|
| 162 |
+
DeletionTaskStatusDTO conflictStatus = DeletionTaskStatusDTO.builder()
|
| 163 |
+
.taskId(sampleTaskId)
|
| 164 |
+
.status("COMPLETED") // Example of a non-approvable state
|
| 165 |
+
.errorMessages(List.of("Task not in approvable state"))
|
| 166 |
+
.build();
|
| 167 |
+
when(taskService.approveTask(eq(sampleTaskId), any())).thenReturn(conflictStatus);
|
| 168 |
+
|
| 169 |
+
mockMvc.perform(post("/api/v1/deletion/tasks/{taskId}/approve", sampleTaskId)
|
| 170 |
+
.with(csrf()))
|
| 171 |
+
.andExpect(status().isConflict()) // Controller maps this from DTO having errorMessages
|
| 172 |
+
.andExpect(jsonPath("$.status").value("COMPLETED"));
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
@Test
|
| 176 |
+
@WithMockUser(authorities = "ROLE_DATA_STEWARD")
|
| 177 |
+
void rejectDeletionTask_AsDataSteward_WhenTaskRejectable_ShouldReturnOk() throws Exception {
|
| 178 |
+
DeletionTaskStatusDTO rejectedStatus = DeletionTaskStatusDTO.builder().taskId(sampleTaskId).status("REJECTED").build();
|
| 179 |
+
when(taskService.rejectTask(eq(sampleTaskId), any())).thenReturn(rejectedStatus);
|
| 180 |
+
|
| 181 |
+
mockMvc.perform(post("/api/v1/deletion/tasks/{taskId}/reject", sampleTaskId)
|
| 182 |
+
.with(csrf()))
|
| 183 |
+
.andExpect(status().isOk())
|
| 184 |
+
.andExpect(jsonPath("$.status").value("REJECTED"));
|
| 185 |
+
}
|
| 186 |
+
}
|
src/test/java/com/dalab/autodelete/service/impl/DeletionConfigServiceImplTest.java
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.service.impl;
|
| 2 |
+
|
| 3 |
+
import com.dalab.autodelete.dto.DeletionConfigDTO;
|
| 4 |
+
import com.dalab.autodelete.mapper.DeletionConfigMapper;
|
| 5 |
+
import com.dalab.autodelete.model.DeletionConfigEntity;
|
| 6 |
+
import com.dalab.autodelete.repository.DeletionConfigRepository;
|
| 7 |
+
import org.junit.jupiter.api.BeforeEach;
|
| 8 |
+
import org.junit.jupiter.api.Test;
|
| 9 |
+
import org.junit.jupiter.api.extension.ExtendWith;
|
| 10 |
+
import org.mockito.InjectMocks;
|
| 11 |
+
import org.mockito.Mock;
|
| 12 |
+
import org.mockito.junit.jupiter.MockitoExtension;
|
| 13 |
+
|
| 14 |
+
import java.util.ArrayList;
|
| 15 |
+
import java.util.Optional;
|
| 16 |
+
|
| 17 |
+
import static org.junit.jupiter.api.Assertions.*;
|
| 18 |
+
import static org.mockito.Mockito.*;
|
| 19 |
+
|
| 20 |
+
@ExtendWith(MockitoExtension.class)
|
| 21 |
+
class DeletionConfigServiceImplTest {
|
| 22 |
+
|
| 23 |
+
@Mock
|
| 24 |
+
private DeletionConfigRepository configRepository;
|
| 25 |
+
|
| 26 |
+
@Mock
|
| 27 |
+
private DeletionConfigMapper configMapper;
|
| 28 |
+
|
| 29 |
+
@InjectMocks
|
| 30 |
+
private DeletionConfigServiceImpl configService;
|
| 31 |
+
|
| 32 |
+
private DeletionConfigEntity sampleEntity;
|
| 33 |
+
private DeletionConfigDTO sampleDTO;
|
| 34 |
+
private static final Long GLOBAL_CONFIG_ID = 1L;
|
| 35 |
+
|
| 36 |
+
@BeforeEach
|
| 37 |
+
void setUp() {
|
| 38 |
+
sampleEntity = new DeletionConfigEntity(
|
| 39 |
+
GLOBAL_CONFIG_ID, true, true, 30L, true, false, new ArrayList<>()
|
| 40 |
+
);
|
| 41 |
+
sampleDTO = DeletionConfigDTO.builder()
|
| 42 |
+
.enabled(true).softDeleteByDefault(true).defaultSoftDeleteRetentionDays(30L)
|
| 43 |
+
.requireApprovalForHardDelete(true).requireApprovalForSoftDelete(false)
|
| 44 |
+
.notificationEmailsOnError(new ArrayList<>()).build();
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
@Test
|
| 48 |
+
void init_WhenConfigExists_ShouldNotSave() {
|
| 49 |
+
when(configRepository.existsById(GLOBAL_CONFIG_ID)).thenReturn(true);
|
| 50 |
+
configService.init();
|
| 51 |
+
verify(configRepository, never()).save(any(DeletionConfigEntity.class));
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
@Test
|
| 55 |
+
void init_WhenConfigNotExists_ShouldSaveDefault() {
|
| 56 |
+
when(configRepository.existsById(GLOBAL_CONFIG_ID)).thenReturn(false);
|
| 57 |
+
when(configRepository.save(any(DeletionConfigEntity.class))).thenReturn(sampleEntity); // Mock save
|
| 58 |
+
configService.init();
|
| 59 |
+
verify(configRepository, times(1)).save(any(DeletionConfigEntity.class));
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
@Test
|
| 63 |
+
void getDeletionConfig_WhenEntityExists_ShouldReturnMappedDTO() {
|
| 64 |
+
when(configRepository.findById(GLOBAL_CONFIG_ID)).thenReturn(Optional.of(sampleEntity));
|
| 65 |
+
when(configMapper.toDto(sampleEntity)).thenReturn(sampleDTO);
|
| 66 |
+
|
| 67 |
+
DeletionConfigDTO result = configService.getDeletionConfig();
|
| 68 |
+
|
| 69 |
+
assertNotNull(result);
|
| 70 |
+
assertEquals(sampleDTO.isSoftDeleteByDefault(), result.isSoftDeleteByDefault());
|
| 71 |
+
verify(configRepository, times(1)).findById(GLOBAL_CONFIG_ID);
|
| 72 |
+
verify(configMapper, times(1)).toDto(sampleEntity);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
@Test
|
| 76 |
+
void getDeletionConfig_WhenEntityNotExists_ShouldReturnEmptyDTO() {
|
| 77 |
+
// This scenario tests the defensive elseGet in the service, though it should ideally not happen after init.
|
| 78 |
+
when(configRepository.findById(GLOBAL_CONFIG_ID)).thenReturn(Optional.empty());
|
| 79 |
+
|
| 80 |
+
DeletionConfigDTO result = configService.getDeletionConfig();
|
| 81 |
+
|
| 82 |
+
assertNotNull(result); // Service returns new DTO(), not null
|
| 83 |
+
// Assert default fields of new DeletionConfigDTO() if necessary, or that it's just not null
|
| 84 |
+
assertNull(result.getEnabled()); // Example: Builder default for Boolean is null
|
| 85 |
+
|
| 86 |
+
verify(configRepository, times(1)).findById(GLOBAL_CONFIG_ID);
|
| 87 |
+
verify(configMapper, never()).toDto(any());
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@Test
|
| 92 |
+
void updateDeletionConfig_WhenEntityExists_ShouldUpdateAndSave() {
|
| 93 |
+
when(configRepository.findById(GLOBAL_CONFIG_ID)).thenReturn(Optional.of(sampleEntity));
|
| 94 |
+
doNothing().when(configMapper).updateEntityFromDto(eq(sampleDTO), eq(sampleEntity));
|
| 95 |
+
when(configRepository.save(any(DeletionConfigEntity.class))).thenReturn(sampleEntity);
|
| 96 |
+
|
| 97 |
+
configService.updateDeletionConfig(sampleDTO);
|
| 98 |
+
|
| 99 |
+
verify(configRepository, times(1)).findById(GLOBAL_CONFIG_ID);
|
| 100 |
+
verify(configMapper, times(1)).updateEntityFromDto(sampleDTO, sampleEntity);
|
| 101 |
+
verify(configRepository, times(1)).save(sampleEntity);
|
| 102 |
+
assertEquals(GLOBAL_CONFIG_ID, sampleEntity.getId()); // Ensure ID is preserved
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
@Test
|
| 106 |
+
void updateDeletionConfig_WhenDtoIsNull_ShouldThrowIllegalArgumentException() {
|
| 107 |
+
assertThrows(IllegalArgumentException.class, () -> {
|
| 108 |
+
configService.updateDeletionConfig(null);
|
| 109 |
+
});
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
@Test
|
| 113 |
+
void updateDeletionConfig_WhenEntityNotFound_ShouldThrowIllegalStateException() {
|
| 114 |
+
when(configRepository.findById(GLOBAL_CONFIG_ID)).thenReturn(Optional.empty());
|
| 115 |
+
assertThrows(IllegalStateException.class, () -> {
|
| 116 |
+
configService.updateDeletionConfig(sampleDTO);
|
| 117 |
+
});
|
| 118 |
+
}
|
| 119 |
+
}
|
src/test/java/com/dalab/autodelete/service/impl/DeletionTaskServiceImplTest.java
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.dalab.autodelete.service.impl;
|
| 2 |
+
|
| 3 |
+
import com.dalab.autodelete.dto.*;
|
| 4 |
+
import com.dalab.autodelete.mapper.DeletionTaskMapper;
|
| 5 |
+
import com.dalab.autodelete.model.DeletionStatus;
|
| 6 |
+
import com.dalab.autodelete.model.DeletionTaskEntity;
|
| 7 |
+
import com.dalab.autodelete.repository.DeletionTaskRepository;
|
| 8 |
+
import com.dalab.autodelete.service.IDeletionConfigService;
|
| 9 |
+
import com.dalab.autodelete.service.exception.DeletionTaskNotFoundException;
|
| 10 |
+
import org.junit.jupiter.api.BeforeEach;
|
| 11 |
+
import org.junit.jupiter.api.Test;
|
| 12 |
+
import org.junit.jupiter.api.extension.ExtendWith;
|
| 13 |
+
import org.mockito.ArgumentCaptor;
|
| 14 |
+
import org.mockito.InjectMocks;
|
| 15 |
+
import org.mockito.Mock;
|
| 16 |
+
import org.mockito.junit.jupiter.MockitoExtension;
|
| 17 |
+
import org.springframework.data.domain.Page;
|
| 18 |
+
import org.springframework.data.domain.PageImpl;
|
| 19 |
+
import org.springframework.data.domain.PageRequest;
|
| 20 |
+
import org.springframework.data.domain.Pageable;
|
| 21 |
+
|
| 22 |
+
import java.time.LocalDateTime;
|
| 23 |
+
import java.util.ArrayList;
|
| 24 |
+
import java.util.Collections;
|
| 25 |
+
import java.util.List;
|
| 26 |
+
import java.util.Optional;
|
| 27 |
+
import java.util.UUID;
|
| 28 |
+
|
| 29 |
+
import static org.junit.jupiter.api.Assertions.*;
|
| 30 |
+
import static org.mockito.ArgumentMatchers.any;
|
| 31 |
+
import static org.mockito.Mockito.*;
|
| 32 |
+
|
| 33 |
+
@ExtendWith(MockitoExtension.class)
|
| 34 |
+
class DeletionTaskServiceImplTest {
|
| 35 |
+
|
| 36 |
+
@Mock
|
| 37 |
+
private DeletionTaskRepository taskRepository;
|
| 38 |
+
|
| 39 |
+
@Mock
|
| 40 |
+
private DeletionTaskMapper taskMapper;
|
| 41 |
+
|
| 42 |
+
@Mock
|
| 43 |
+
private IDeletionConfigService deletionConfigService;
|
| 44 |
+
|
| 45 |
+
@InjectMocks
|
| 46 |
+
private DeletionTaskServiceImpl taskService;
|
| 47 |
+
|
| 48 |
+
private DeletionTaskRequest sampleTaskRequest;
|
| 49 |
+
private DeletionTaskEntity sampleTaskEntity;
|
| 50 |
+
private DeletionTaskStatusDTO sampleTaskStatusDTO;
|
| 51 |
+
private DeletionTaskResponse sampleTaskResponse;
|
| 52 |
+
private DeletionConfigDTO globalConfigDefault;
|
| 53 |
+
private DeletionConfigDTO globalConfigSoftDeleteApproval;
|
| 54 |
+
private String sampleTaskId;
|
| 55 |
+
|
| 56 |
+
@BeforeEach
|
| 57 |
+
void setUp() {
|
| 58 |
+
sampleTaskId = UUID.randomUUID().toString();
|
| 59 |
+
DeletionTaskRequest.DeletionScope scope = DeletionTaskRequest.DeletionScope.builder()
|
| 60 |
+
.assetIds(List.of("asset1")).build();
|
| 61 |
+
|
| 62 |
+
sampleTaskRequest = DeletionTaskRequest.builder()
|
| 63 |
+
.taskName("Test Task")
|
| 64 |
+
.scope(scope)
|
| 65 |
+
.triggeredBy("user@test.com")
|
| 66 |
+
.build();
|
| 67 |
+
|
| 68 |
+
sampleTaskEntity = new DeletionTaskEntity();
|
| 69 |
+
sampleTaskEntity.setTaskId(sampleTaskId);
|
| 70 |
+
sampleTaskEntity.setTaskName("Test Task");
|
| 71 |
+
sampleTaskEntity.setStatus(DeletionStatus.SUBMITTED);
|
| 72 |
+
sampleTaskEntity.setSubmittedAt(LocalDateTime.now());
|
| 73 |
+
sampleTaskEntity.setScope(new DeletionTaskEntity.DeletionScopeData(List.of("asset1")));
|
| 74 |
+
sampleTaskEntity.setAssetStatuses(new ArrayList<>());
|
| 75 |
+
|
| 76 |
+
sampleTaskStatusDTO = DeletionTaskStatusDTO.builder().taskId(sampleTaskId).status(DeletionStatus.SUBMITTED.name()).build();
|
| 77 |
+
sampleTaskResponse = DeletionTaskResponse.builder().taskId(sampleTaskId).status(DeletionStatus.SUBMITTED.name()).build();
|
| 78 |
+
|
| 79 |
+
globalConfigDefault = DeletionConfigDTO.builder()
|
| 80 |
+
.enabled(true)
|
| 81 |
+
.softDeleteByDefault(false) // Hard delete by default
|
| 82 |
+
.requireApprovalForHardDelete(false) // No approval for hard delete
|
| 83 |
+
.requireApprovalForSoftDelete(true) // Approval for soft delete
|
| 84 |
+
.build();
|
| 85 |
+
|
| 86 |
+
globalConfigSoftDeleteApproval = DeletionConfigDTO.builder()
|
| 87 |
+
.enabled(true)
|
| 88 |
+
.softDeleteByDefault(true) // Soft delete by default
|
| 89 |
+
.requireApprovalForHardDelete(true) // Approval for hard delete
|
| 90 |
+
.requireApprovalForSoftDelete(true) // Approval for soft delete
|
| 91 |
+
.build();
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
@Test
|
| 95 |
+
void submitDeletionTask_ValidRequest_NoApprovalNeeded_ShouldSaveAndReturnResponse() {
|
| 96 |
+
when(deletionConfigService.getDeletionConfig()).thenReturn(globalConfigDefault);
|
| 97 |
+
when(taskMapper.requestToEntity(sampleTaskRequest)).thenReturn(sampleTaskEntity); // sampleTaskEntity is already set up
|
| 98 |
+
when(taskRepository.save(any(DeletionTaskEntity.class))).thenReturn(sampleTaskEntity);
|
| 99 |
+
when(taskMapper.entityToTaskResponse(sampleTaskEntity)).thenReturn(sampleTaskResponse);
|
| 100 |
+
|
| 101 |
+
DeletionTaskResponse response = taskService.submitDeletionTask(sampleTaskRequest);
|
| 102 |
+
|
| 103 |
+
assertNotNull(response);
|
| 104 |
+
assertEquals(DeletionStatus.SUBMITTED.name(), response.getStatus());
|
| 105 |
+
ArgumentCaptor<DeletionTaskEntity> entityCaptor = ArgumentCaptor.forClass(DeletionTaskEntity.class);
|
| 106 |
+
verify(taskRepository).save(entityCaptor.capture());
|
| 107 |
+
assertEquals(DeletionStatus.SUBMITTED, entityCaptor.getValue().getStatus());
|
| 108 |
+
assertNotNull(entityCaptor.getValue().getAssetStatuses());
|
| 109 |
+
assertEquals(1, entityCaptor.getValue().getAssetsPendingProcessing());
|
| 110 |
+
assertEquals("HARD", entityCaptor.getValue().getAssetStatuses().get(0).getDeletionType());
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
@Test
|
| 114 |
+
void submitDeletionTask_RequiresHardDeleteApproval_ShouldSetPendingApproval() {
|
| 115 |
+
DeletionConfigDTO config = DeletionConfigDTO.builder().softDeleteByDefault(false).requireApprovalForHardDelete(true).build();
|
| 116 |
+
when(deletionConfigService.getDeletionConfig()).thenReturn(config);
|
| 117 |
+
when(taskMapper.requestToEntity(sampleTaskRequest)).thenReturn(sampleTaskEntity);
|
| 118 |
+
when(taskRepository.save(any(DeletionTaskEntity.class))).thenReturn(sampleTaskEntity);
|
| 119 |
+
|
| 120 |
+
taskService.submitDeletionTask(sampleTaskRequest);
|
| 121 |
+
|
| 122 |
+
ArgumentCaptor<DeletionTaskEntity> entityCaptor = ArgumentCaptor.forClass(DeletionTaskEntity.class);
|
| 123 |
+
verify(taskRepository).save(entityCaptor.capture());
|
| 124 |
+
assertEquals(DeletionStatus.PENDING_APPROVAL, entityCaptor.getValue().getStatus());
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
@Test
|
| 128 |
+
void submitDeletionTask_SoftDeleteOverride_RequiresSoftApproval_ShouldSetPendingApproval() {
|
| 129 |
+
sampleTaskRequest.setOverrideConfig(DeletionTaskRequest.DeletionConfigOverride.builder().softDelete(true).build());
|
| 130 |
+
when(deletionConfigService.getDeletionConfig()).thenReturn(globalConfigSoftDeleteApproval);
|
| 131 |
+
when(taskMapper.requestToEntity(sampleTaskRequest)).thenReturn(sampleTaskEntity);
|
| 132 |
+
when(taskRepository.save(any(DeletionTaskEntity.class))).thenReturn(sampleTaskEntity);
|
| 133 |
+
|
| 134 |
+
taskService.submitDeletionTask(sampleTaskRequest);
|
| 135 |
+
ArgumentCaptor<DeletionTaskEntity> entityCaptor = ArgumentCaptor.forClass(DeletionTaskEntity.class);
|
| 136 |
+
verify(taskRepository).save(entityCaptor.capture());
|
| 137 |
+
assertEquals(DeletionStatus.PENDING_APPROVAL, entityCaptor.getValue().getStatus());
|
| 138 |
+
assertEquals("SOFT", entityCaptor.getValue().getAssetStatuses().get(0).getDeletionType());
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
@Test
|
| 142 |
+
void submitDeletionTask_InvalidScope_ShouldReturnFailedStatus() {
|
| 143 |
+
DeletionTaskRequest invalidRequest = DeletionTaskRequest.builder().taskName("Invalid").scope(null).build();
|
| 144 |
+
DeletionTaskResponse response = taskService.submitDeletionTask(invalidRequest);
|
| 145 |
+
assertEquals(DeletionStatus.FAILED.name(), response.getStatus());
|
| 146 |
+
verify(taskRepository, never()).save(any());
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
@Test
|
| 150 |
+
void getTaskStatus_TaskExists_ShouldReturnDTO() {
|
| 151 |
+
when(taskRepository.findById(sampleTaskId)).thenReturn(Optional.of(sampleTaskEntity));
|
| 152 |
+
when(taskMapper.entityToStatusDTO(sampleTaskEntity)).thenReturn(sampleTaskStatusDTO);
|
| 153 |
+
|
| 154 |
+
DeletionTaskStatusDTO result = taskService.getTaskStatus(sampleTaskId);
|
| 155 |
+
|
| 156 |
+
assertNotNull(result);
|
| 157 |
+
assertEquals(sampleTaskId, result.getTaskId());
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
@Test
|
| 161 |
+
void getTaskStatus_TaskNotExists_ShouldReturnNull() {
|
| 162 |
+
when(taskRepository.findById(sampleTaskId)).thenReturn(Optional.empty());
|
| 163 |
+
DeletionTaskStatusDTO result = taskService.getTaskStatus(sampleTaskId);
|
| 164 |
+
assertNull(result);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
@Test
|
| 168 |
+
void listTasks_ShouldReturnPaginatedDTOs() {
|
| 169 |
+
Pageable pageable = PageRequest.of(0, 10);
|
| 170 |
+
List<DeletionTaskEntity> taskList = Collections.singletonList(sampleTaskEntity);
|
| 171 |
+
Page<DeletionTaskEntity> page = new PageImpl<>(taskList, pageable, 1);
|
| 172 |
+
when(taskRepository.findAll(pageable)).thenReturn(page);
|
| 173 |
+
when(taskMapper.entityToStatusDTO(sampleTaskEntity)).thenReturn(sampleTaskStatusDTO);
|
| 174 |
+
|
| 175 |
+
DeletionTaskListResponse response = taskService.listTasks(pageable);
|
| 176 |
+
|
| 177 |
+
assertNotNull(response);
|
| 178 |
+
assertEquals(1, response.getTasks().size());
|
| 179 |
+
assertEquals(sampleTaskId, response.getTasks().get(0).getTaskId());
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
@Test
|
| 183 |
+
void approveTask_TaskExistsAndPending_ShouldApprove() {
|
| 184 |
+
sampleTaskEntity.setStatus(DeletionStatus.PENDING_APPROVAL);
|
| 185 |
+
when(taskRepository.findById(sampleTaskId)).thenReturn(Optional.of(sampleTaskEntity));
|
| 186 |
+
when(taskRepository.save(any(DeletionTaskEntity.class))).thenReturn(sampleTaskEntity);
|
| 187 |
+
when(taskMapper.entityToStatusDTO(any(DeletionTaskEntity.class))).thenAnswer(invocation -> {
|
| 188 |
+
DeletionTaskEntity entity = invocation.getArgument(0);
|
| 189 |
+
return DeletionTaskStatusDTO.builder().taskId(entity.getTaskId()).status(entity.getStatus().name()).build();
|
| 190 |
+
});
|
| 191 |
+
|
| 192 |
+
TaskApprovalRequest approvalReq = TaskApprovalRequest.builder().comments("Approved by test").build();
|
| 193 |
+
DeletionTaskStatusDTO result = taskService.approveTask(sampleTaskId, approvalReq);
|
| 194 |
+
|
| 195 |
+
assertEquals(DeletionStatus.APPROVED.name(), result.getStatus());
|
| 196 |
+
assertEquals("Approved by test", sampleTaskEntity.getApprovalComments());
|
| 197 |
+
verify(taskRepository).save(sampleTaskEntity);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
@Test
|
| 201 |
+
void approveTask_TaskNotPending_ShouldReturnCurrentStatusWithErrors() {
|
| 202 |
+
sampleTaskEntity.setStatus(DeletionStatus.COMPLETED);
|
| 203 |
+
when(taskRepository.findById(sampleTaskId)).thenReturn(Optional.of(sampleTaskEntity));
|
| 204 |
+
// Mapper will be called to return the current DTO
|
| 205 |
+
when(taskMapper.entityToStatusDTO(sampleTaskEntity)).thenReturn(DeletionTaskStatusDTO.builder().taskId(sampleTaskId).status(DeletionStatus.COMPLETED.name()).build());
|
| 206 |
+
|
| 207 |
+
DeletionTaskStatusDTO result = taskService.approveTask(sampleTaskId, new TaskApprovalRequest());
|
| 208 |
+
|
| 209 |
+
assertEquals(DeletionStatus.COMPLETED.name(), result.getStatus());
|
| 210 |
+
assertNotNull(result.getErrorMessages());
|
| 211 |
+
assertFalse(result.getErrorMessages().isEmpty());
|
| 212 |
+
verify(taskRepository, never()).save(any());
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
@Test
|
| 216 |
+
void approveTask_TaskNotFound_ShouldThrowException() {
|
| 217 |
+
when(taskRepository.findById(sampleTaskId)).thenReturn(Optional.empty());
|
| 218 |
+
assertThrows(DeletionTaskNotFoundException.class, () -> {
|
| 219 |
+
taskService.approveTask(sampleTaskId, new TaskApprovalRequest());
|
| 220 |
+
});
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
@Test
|
| 224 |
+
void rejectTask_TaskExistsAndPending_ShouldReject() {
|
| 225 |
+
sampleTaskEntity.setStatus(DeletionStatus.PENDING_APPROVAL);
|
| 226 |
+
when(taskRepository.findById(sampleTaskId)).thenReturn(Optional.of(sampleTaskEntity));
|
| 227 |
+
when(taskRepository.save(any(DeletionTaskEntity.class))).thenReturn(sampleTaskEntity);
|
| 228 |
+
when(taskMapper.entityToStatusDTO(any(DeletionTaskEntity.class))).thenAnswer(invocation -> {
|
| 229 |
+
DeletionTaskEntity entity = invocation.getArgument(0);
|
| 230 |
+
return DeletionTaskStatusDTO.builder().taskId(entity.getTaskId()).status(entity.getStatus().name()).build();
|
| 231 |
+
});
|
| 232 |
+
|
| 233 |
+
TaskApprovalRequest rejectionReq = TaskApprovalRequest.builder().comments("Rejected by test").build();
|
| 234 |
+
DeletionTaskStatusDTO result = taskService.rejectTask(sampleTaskId, rejectionReq);
|
| 235 |
+
|
| 236 |
+
assertEquals(DeletionStatus.REJECTED.name(), result.getStatus());
|
| 237 |
+
assertEquals("Rejected by test", sampleTaskEntity.getRejectionComments());
|
| 238 |
+
verify(taskRepository).save(sampleTaskEntity);
|
| 239 |
+
}
|
| 240 |
+
}
|