PortfolioReviewedImportController.java
package com.taxonomy.portfolio.controller;
import com.taxonomy.portfolio.dto.PortfolioDtos.AnalyzeProjectRequest;
import com.taxonomy.portfolio.dto.PortfolioImportReviewDtos.ReviewedImportItem;
import com.taxonomy.portfolio.dto.PortfolioImportReviewDtos.ReviewedImportRequest;
import com.taxonomy.portfolio.dto.PortfolioImportReviewDtos.ReviewedImportResult;
import com.taxonomy.portfolio.service.PortfolioException;
import com.taxonomy.portfolio.service.PortfolioReviewedImportService;
import com.taxonomy.portfolio.service.ProjectRequirementAnalysisService;
import com.taxonomy.workspace.service.WorkspaceContext;
import com.taxonomy.workspace.service.WorkspaceResolver;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.net.URI;
/** Atomic adapter for mixed new-requirement and new-version import decisions. */
@RestController
@RequestMapping("/api/projects/{projectId}/requirements")
@Tag(name = "Project Requirement Import Review")
public class PortfolioReviewedImportController {
private final PortfolioReviewedImportService importService;
private final ProjectRequirementAnalysisService analysisService;
private final WorkspaceResolver workspaceResolver;
private final int maximumItems;
private final long maximumCharacters;
public PortfolioReviewedImportController(
PortfolioReviewedImportService importService,
ProjectRequirementAnalysisService analysisService,
WorkspaceResolver workspaceResolver,
@Value("${taxonomy.portfolio.max-import-requirements:100}") int maximumItems,
@Value("${taxonomy.portfolio.max-import-characters:500000}") long maximumCharacters) {
this.importService = importService;
this.analysisService = analysisService;
this.workspaceResolver = workspaceResolver;
this.maximumItems = Math.max(1, maximumItems);
this.maximumCharacters = Math.max(1L, maximumCharacters);
}
@PostMapping("/import-review")
@Operation(summary = "Atomically apply a reviewed mixed document import")
public ResponseEntity<ReviewedImportResult> importReviewed(
@PathVariable Long projectId,
@RequestBody ReviewedImportRequest request) {
validate(request);
String username = workspaceResolver.resolveCurrentUsername();
WorkspaceContext context = workspaceResolver.resolveCurrentContext();
var persisted = importService.persist(projectId, request.items(), username, context);
var job = request.analyzeAfterImport() && !persisted.affectedRequirementIds().isEmpty()
? analysisService.enqueueProject(
projectId,
new AnalyzeProjectRequest(
persisted.affectedRequirementIds(),
false,
request.provider(),
request.maxArchitectureNodes(),
request.idempotencyKey()),
username,
context)
: null;
ReviewedImportResult result = new ReviewedImportResult(
persisted.newRequirements(), persisted.versionedRequirements(), job);
if (job != null) {
return ResponseEntity.accepted()
.location(URI.create("/api/projects/" + projectId + "/analysis-jobs/" + job.id()))
.body(result);
}
return ResponseEntity.status(201).body(result);
}
private void validate(ReviewedImportRequest request) {
if (request == null || request.items() == null || request.items().isEmpty()) {
throw PortfolioException.validation("At least one reviewed import item is required");
}
if (request.items().size() > maximumItems) {
throw PortfolioException.validation(
"Reviewed import contains more than " + maximumItems + " items");
}
long characters = 0L;
for (ReviewedImportItem item : request.items()) {
if (item == null) continue;
characters += length(item.text());
if (item.source() != null) characters += length(item.source().originalText());
if (characters > maximumCharacters) {
throw PortfolioException.validation(
"Reviewed import exceeds " + maximumCharacters + " text characters");
}
}
}
private static int length(String value) {
return value != null ? value.length() : 0;
}
}