ArchitectureWorkbenchService.java
package com.taxonomy.portfolio.workbench;
import com.taxonomy.diagram.DiagramModel;
import com.taxonomy.diagram.DiagramScene;
import com.taxonomy.dto.AnalysisResult;
import com.taxonomy.dto.RequirementArchitectureView;
import com.taxonomy.export.DiagramProjectionService;
import com.taxonomy.export.LayeredDiagramLayoutService;
import com.taxonomy.export.PersistedDiagramProjection;
import com.taxonomy.export.SvgDiagramRenderer;
import com.taxonomy.portfolio.dto.PortfolioDtos.ElementMappingView;
import com.taxonomy.portfolio.dto.PortfolioDtos.ProjectView;
import com.taxonomy.portfolio.dto.PortfolioDtos.RelationMappingView;
import com.taxonomy.portfolio.dto.PortfolioDtos.RequirementVersionView;
import com.taxonomy.portfolio.dto.PortfolioDtos.RequirementView;
import com.taxonomy.portfolio.dto.PortfolioDtos.SnapshotDetail;
import com.taxonomy.portfolio.service.PortfolioAnalysisPersistenceService;
import com.taxonomy.portfolio.service.PortfolioException;
import com.taxonomy.portfolio.service.ProjectPortfolioService;
import com.taxonomy.portfolio.workbench.ArchitectureWorkbenchDtos.ElementMetadata;
import com.taxonomy.portfolio.workbench.ArchitectureWorkbenchDtos.Projection;
import com.taxonomy.portfolio.workbench.ArchitectureWorkbenchDtos.RelationMetadata;
import com.taxonomy.workspace.service.WorkspaceContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Replays an immutable portfolio analysis snapshot for browser, SVG and PDF adapters. */
@Service
public class ArchitectureWorkbenchService {
private final PortfolioAnalysisPersistenceService persistenceService;
private final ProjectPortfolioService projectService;
private final DiagramProjectionService diagramProjectionService;
private final LayeredDiagramLayoutService layoutService;
private final SvgDiagramRenderer svgRenderer;
private final ArchitecturePdfRenderer pdfRenderer;
public ArchitectureWorkbenchService(
PortfolioAnalysisPersistenceService persistenceService,
ProjectPortfolioService projectService,
DiagramProjectionService diagramProjectionService,
LayeredDiagramLayoutService layoutService,
SvgDiagramRenderer svgRenderer,
ArchitecturePdfRenderer pdfRenderer) {
this.persistenceService = persistenceService;
this.projectService = projectService;
this.diagramProjectionService = diagramProjectionService;
this.layoutService = layoutService;
this.svgRenderer = svgRenderer;
this.pdfRenderer = pdfRenderer;
}
@Transactional(readOnly = true)
public Projection load(Long projectId,
String snapshotId,
String username,
WorkspaceContext context) {
if (snapshotId == null || snapshotId.isBlank()) {
throw PortfolioException.validation("snapshotId is required");
}
SnapshotDetail snapshot = persistenceService.getSnapshot(
projectId, snapshotId.strip(), username, context);
AnalysisResult analysis = snapshot.analysis();
RequirementArchitectureView architectureView =
analysis != null ? analysis.getArchitectureView() : null;
if (architectureView == null
|| architectureView.getIncludedElements() == null
|| architectureView.getIncludedElements().isEmpty()) {
throw PortfolioException.conflict(
"Snapshot " + snapshotId
+ " contains no persisted architecture view. "
+ "Re-run the requirement analysis with Architecture View enabled.");
}
ProjectView project = projectService.getProject(projectId, username, context);
Long requirementId = snapshot.summary().requirementId();
RequirementView requirement = projectService.getRequirement(
projectId, requirementId, username, context);
RequirementVersionView version = projectService
.listRequirementVersions(projectId, requirementId, username, context)
.stream()
.filter(candidate -> candidate.id().equals(snapshot.summary().requirementVersionId()))
.findFirst()
.orElseThrow(() -> PortfolioException.notFound(
"Requirement version for snapshot was not found: "
+ snapshot.summary().requirementVersionId()));
String fallbackTitle = project.projectKey() + " / " + requirement.requirementKey()
+ " — " + requirement.title();
String persistedTitle = architectureView.getViewTitle();
String title = persistedTitle == null || persistedTitle.isBlank()
? fallbackTitle
: persistedTitle.strip();
DiagramModel diagram = PersistedDiagramProjection.project(
diagramProjectionService, architectureView, title);
if (diagram.nodes() == null || diagram.nodes().isEmpty()) {
throw PortfolioException.conflict(
"The persisted architecture view contains no exportable architecture elements");
}
DiagramScene scene = layoutService.layout(diagram);
Map<String, ElementMetadata> elements = new LinkedHashMap<>();
for (ElementMappingView mapping : safe(snapshot.elementMappings())) {
elements.put(mapping.nodeCode(), new ElementMetadata(
mapping.nodeCode(), mapping.nodeTitle(), mapping.taxonomyRoot(),
mapping.directScore(), mapping.relevance(), mapping.confidence(),
mapping.mappingOrigin() != null ? mapping.mappingOrigin().name() : null,
mapping.hierarchyPath(), mapping.presenceReason(), mapping.selectedForImpact(),
mapping.reviewStatus(), mapping.actionStatus(), mapping.actionEvidence(),
mapping.decisionBy(), mapping.decisionAt(), mapping.decisionComment()));
}
Map<String, RelationMetadata> relations = new LinkedHashMap<>();
for (RelationMappingView mapping : safe(snapshot.relationMappings())) {
RelationMetadata metadata = new RelationMetadata(
mapping.sourceCode(), mapping.targetCode(), mapping.relationType(),
mapping.relationOrigin(), mapping.relationCategory(), mapping.relevance(),
mapping.confidence(), mapping.presenceReason(), mapping.reviewStatus(),
mapping.decisionBy(), mapping.decisionAt(), mapping.decisionComment());
relations.put(metadata.signature(), metadata);
}
Set<String> warnings = new LinkedHashSet<>();
if (analysis.getWarnings() != null) warnings.addAll(analysis.getWarnings());
if (architectureView.getNotes() != null) warnings.addAll(architectureView.getNotes());
warnings.removeIf(value -> value == null || value.isBlank());
return new Projection(
projectId, project.projectKey(), project.title(), requirementId,
requirement.requirementKey(), requirement.title(), version.text(),
snapshot.summary().id(), snapshot.summary().status(), snapshot.summary().createdAt(),
snapshot.summary().provider(), snapshot.summary().modelName(),
snapshot.summary().workspaceId(), snapshot.summary().branchName(),
snapshot.summary().commitSha(), diagram, scene, elements, relations,
new ArrayList<>(warnings), new ArchitectureWorkbenchDtos.SnapshotProvenance(
snapshot.summary().requirementVersionId(), snapshot.summary().taxonomyFingerprint(),
context.repositoryId()));
}
@Transactional(readOnly = true)
public String renderSvg(Long projectId,
String snapshotId,
String username,
WorkspaceContext context) {
return svgRenderer.render(load(projectId, snapshotId, username, context).scene());
}
@Transactional(readOnly = true)
public byte[] renderPdf(Long projectId,
String snapshotId,
String username,
WorkspaceContext context) {
return pdfRenderer.render(load(projectId, snapshotId, username, context));
}
private static <T> List<T> safe(List<T> values) {
return values == null ? List.of() : values;
}
}