ExportApiController.java
package com.taxonomy.export.controller;
import com.taxonomy.diagram.DiagramModel;
import com.taxonomy.dto.SavedAnalysis;
import com.taxonomy.export.MermaidLabels;
import com.taxonomy.export.service.ExportFacade;
import com.taxonomy.export.service.ExportFormatExtensionRegistry;
import com.taxonomy.export.spi.ExportContext;
import com.taxonomy.export.spi.ExportFormatDescriptor;
import com.taxonomy.export.spi.ExportFormatExtension;
import com.taxonomy.export.spi.ExportResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
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.io.IOException;
import java.io.UncheckedIOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@RestController
@RequestMapping("/api")
@Tag(name = "Export")
public class ExportApiController {
private static final Logger log = LoggerFactory.getLogger(ExportApiController.class);
private final ExportFacade exportFacade;
private final ExportFormatExtensionRegistry exportFormatRegistry;
public ExportApiController(ExportFacade exportFacade,
ExportFormatExtensionRegistry exportFormatRegistry) {
this.exportFacade = exportFacade;
this.exportFormatRegistry = exportFormatRegistry;
}
@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");
return ResponseEntity.ok().headers(headers).body(vsdx);
} catch (IOException e) {
return ResponseEntity.internalServerError().build();
}
}
@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);
}
@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);
}
private MermaidLabels resolveMermaidLabels(Object localeObj) {
if (localeObj instanceof String locale && locale.startsWith("de")) {
return MermaidLabels.german();
}
return MermaidLabels.english();
}
@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));
}
@Operation(summary = "Export diagram in any registered format",
description = "Exports a diagram in the format identified by {formatId}. Registered formats: archimate, mermaid, structurizr, visio, plus any custom ExportFormatExtension components. An optional 'locale' field is forwarded to the adapter.",
tags = {"Export"})
@ApiResponse(responseCode = "200", description = "Diagram file returned as attachment")
@ApiResponse(responseCode = "400", description = "Business text is blank or missing")
@ApiResponse(responseCode = "404", description = "Unknown format ID")
@PostMapping("/diagram/export/{formatId}")
public ResponseEntity<byte[]> exportByFormat(
@PathVariable String formatId,
@RequestBody Map<String, Object> body) {
String businessText = (String) body.get("businessText");
if (businessText == null || businessText.isBlank()) {
return ResponseEntity.badRequest().build();
}
Optional<ExportFormatExtension> extensionOpt = exportFormatRegistry.findByFormatId(formatId);
if (extensionOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
ExportFormatExtension extension = extensionOpt.get();
ExportFormatDescriptor descriptor = extension.descriptor();
Map<String, Object> options = new HashMap<>(body);
options.remove("businessText");
try {
DiagramModel diagram = exportFacade.buildDiagram(businessText);
ExportResult result = extension.export(new ExportContext(diagram, options));
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"diagram." + descriptor.fileExtension() + "\"");
headers.set(HttpHeaders.CONTENT_TYPE, descriptor.contentType());
return ResponseEntity.ok().headers(headers).body(result.bytes());
} catch (UncheckedIOException e) {
log.error("Export failed for format '{}': {}", formatId, e.getMessage(), e);
return ResponseEntity.internalServerError().build();
}
}
@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> entry : rawScores.entrySet()) {
if (entry.getValue() instanceof Number number) {
scores.put(entry.getKey(), number.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();
return ResponseEntity.ok(exportFacade.buildExport(requirement, scores, reasons, provider));
}
@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()));
}
}
}