ExportApiController.java
package com.taxonomy.export.controller;
import com.taxonomy.dto.SavedAnalysis;
import com.taxonomy.export.MermaidLabels;
import com.taxonomy.export.service.ExportFacade;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api")
@Tag(name = "Export")
public class ExportApiController {
private final ExportFacade exportFacade;
public ExportApiController(ExportFacade exportFacade) {
this.exportFacade = exportFacade;
}
// ── Visio Diagram Export ──────────────────────────────────────────────────
@Operation(summary = "Export Visio diagram", description = "Generates a Visio .vsdx architecture diagram from a business requirement", tags = {"Export"})
@ApiResponse(responseCode = "200", description = "Visio file returned as binary attachment")
@ApiResponse(responseCode = "400", description = "Business text is blank or missing")
@PostMapping("/diagram/visio")
public ResponseEntity<byte[]> exportVisio(@RequestBody Map<String, Object> body) {
String businessText = (String) body.get("businessText");
if (businessText == null || businessText.isBlank()) {
return ResponseEntity.badRequest().build();
}
try {
byte[] vsdx = exportFacade.exportAsVisio(businessText);
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"requirement-architecture.vsdx\"");
headers.set(HttpHeaders.CONTENT_TYPE, "application/vnd.ms-visio.drawing.main+xml");
return ResponseEntity.ok().headers(headers).body(vsdx);
} catch (IOException e) {
return ResponseEntity.internalServerError().build();
}
}
// ── ArchiMate Diagram Export ──────────────────────────────────────────────
@Operation(summary = "Export ArchiMate XML", description = "Generates an ArchiMate Model Exchange File Format XML from a business requirement", tags = {"Export"})
@ApiResponse(responseCode = "200", description = "ArchiMate XML returned as attachment")
@ApiResponse(responseCode = "400", description = "Business text is blank or missing")
@PostMapping("/diagram/archimate")
public ResponseEntity<byte[]> exportArchiMate(@RequestBody Map<String, Object> body) {
String businessText = (String) body.get("businessText");
if (businessText == null || businessText.isBlank()) {
return ResponseEntity.badRequest().build();
}
byte[] xml = exportFacade.exportAsArchiMate(businessText);
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"requirement-architecture.xml\"");
headers.set(HttpHeaders.CONTENT_TYPE, "application/xml");
return ResponseEntity.ok().headers(headers).body(xml);
}
// ── Mermaid Diagram Export ────────────────────────────────────────────────
@Operation(summary = "Export Mermaid diagram", description = "Generates a Mermaid flowchart from a business requirement for use in Markdown documents. Accepts optional 'locale' field ('en' or 'de') to localize layer and relation labels.", tags = {"Export"})
@ApiResponse(responseCode = "200", description = "Mermaid text returned")
@ApiResponse(responseCode = "400", description = "Business text is blank or missing")
@PostMapping("/diagram/mermaid")
public ResponseEntity<String> exportMermaid(@RequestBody Map<String, Object> body) {
String businessText = (String) body.get("businessText");
if (businessText == null || businessText.isBlank()) {
return ResponseEntity.badRequest().build();
}
MermaidLabels labels = resolveMermaidLabels(body.get("locale"));
String mermaid = exportFacade.exportAsMermaid(businessText, labels);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, "text/plain; charset=UTF-8")
.body(mermaid);
}
/**
* Resolves {@link MermaidLabels} from an optional locale parameter.
*/
private MermaidLabels resolveMermaidLabels(Object localeObj) {
if (localeObj instanceof String locale && locale.startsWith("de")) {
return MermaidLabels.german();
}
return MermaidLabels.english();
}
// ── Structurizr DSL Export ────────────────────────────────────────────────
@Operation(summary = "Export Structurizr DSL", description = "Generates a Structurizr workspace DSL from a business requirement for C4 tools", tags = {"Export"})
@ApiResponse(responseCode = "200", description = "Structurizr DSL returned as text")
@ApiResponse(responseCode = "400", description = "Business text is blank or missing")
@PostMapping("/diagram/structurizr")
public ResponseEntity<byte[]> exportStructurizrDsl(@RequestBody Map<String, Object> body) {
String businessText = (String) body.get("businessText");
if (businessText == null || businessText.isBlank()) {
return ResponseEntity.badRequest().build();
}
String dsl = exportFacade.exportAsStructurizrDsl(businessText);
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"workspace.dsl\"");
headers.set(HttpHeaders.CONTENT_TYPE, "text/plain; charset=UTF-8");
return ResponseEntity.ok().headers(headers).body(dsl.getBytes(java.nio.charset.StandardCharsets.UTF_8));
}
// ── Scores import / export endpoints ────────────────────────────────────────
@Operation(summary = "Export analysis scores as JSON",
description = "Returns a SavedAnalysis JSON with timestamp and version added. The frontend triggers a file download.",
tags = {"Export"})
@ApiResponse(responseCode = "200", description = "SavedAnalysis JSON returned")
@ApiResponse(responseCode = "400", description = "Requirement is blank or scores are missing")
@PostMapping("/scores/export")
public ResponseEntity<SavedAnalysis> exportScores(@RequestBody Map<String, Object> body) {
String requirement = (String) body.get("requirement");
if (requirement == null || requirement.isBlank()) {
return ResponseEntity.badRequest().build();
}
@SuppressWarnings("unchecked")
Map<String, Object> rawScores = body.get("scores") instanceof Map<?, ?>
? (Map<String, Object>) body.get("scores") : null;
if (rawScores == null || rawScores.isEmpty()) {
return ResponseEntity.badRequest().build();
}
Map<String, Integer> scores = new LinkedHashMap<>();
for (Map.Entry<String, Object> e : rawScores.entrySet()) {
if (e.getValue() instanceof Number n) {
scores.put(e.getKey(), n.intValue());
}
}
@SuppressWarnings("unchecked")
Map<String, String> reasons = body.get("reasons") instanceof Map<?, ?>
? (Map<String, String>) body.get("reasons") : Map.of();
String provider = body.get("provider") instanceof String p ? p : exportFacade.getActiveProviderName();
SavedAnalysis saved = exportFacade.buildExport(requirement, scores, reasons, provider);
return ResponseEntity.ok(saved);
}
@Operation(summary = "Import analysis scores from JSON",
description = "Validates a SavedAnalysis JSON and returns the scores, reasons, requirement, and any warnings.",
tags = {"Export"})
@ApiResponse(responseCode = "200", description = "Scores imported and returned with any warnings")
@ApiResponse(responseCode = "400", description = "Invalid JSON format or validation failure")
@PostMapping("/scores/import")
public ResponseEntity<Map<String, Object>> importScores(@RequestBody String jsonBody) {
try {
SavedAnalysis saved = exportFacade.importFromJson(jsonBody);
List<String> warnings = exportFacade.findUnknownCodes(saved)
.stream()
.map(code -> "Unknown node code: " + code)
.toList();
Map<String, Object> result = new LinkedHashMap<>();
result.put("requirement", saved.getRequirement());
result.put("scores", saved.getScores() != null ? saved.getScores() : Map.of());
result.put("reasons", saved.getReasons() != null ? saved.getReasons() : Map.of());
result.put("provider", saved.getProvider());
result.put("warnings", warnings);
return ResponseEntity.ok(result);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(Map.of("error", e.getMessage(), "warnings", List.of()));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(Map.of("error", "Invalid JSON: " + e.getMessage(), "warnings", List.of()));
}
}
}