ArchiMateDiagramService.java

package com.taxonomy.export;

import com.taxonomy.archimate.exchange.ArchiMateExchangeProfile;
import com.taxonomy.archimate.exchange.ArchiMateIds;
import com.taxonomy.archimate.exchange.ArchiMateSchema;

import com.taxonomy.archimate.ArchiMateElement;
import com.taxonomy.archimate.ArchiMateProperty;
import com.taxonomy.archimate.ArchiMateLoss;
import com.taxonomy.archimate.ArchiMateModel;
import com.taxonomy.archimate.ArchiMateRelationship;
import com.taxonomy.archimate.ArchiMateView;
import com.taxonomy.archimate.ArchiMateViewConnection;
import com.taxonomy.archimate.ArchiMateViewNode;
import com.taxonomy.diagram.DiagramEdge;
import com.taxonomy.diagram.DiagramModel;
import com.taxonomy.diagram.DiagramNode;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Converts a neutral {@link DiagramModel} into an {@link ArchiMateModel} by mapping
 * taxonomy types to ArchiMate element types, relationship types to ArchiMate relationship
 * types, and computing pixel-based layout positions for the diagram view.
 */
public class ArchiMateDiagramService {

    // Layout constants (pixels)
    private static final int NODE_WIDTH  = 120;
    private static final int NODE_HEIGHT =  40;
    private static final int H_GAP       = 160;
    private static final int V_GAP       =  60;
    private static final int MARGIN_X    =  40;
    private static final int MARGIN_Y    =  40;

    /**
     * Converts a {@link DiagramModel} to an {@link ArchiMateModel}.
     */
    public ArchiMateModel convert(DiagramModel model) {
        if (model.nodes().size() > ArchiMateSchema.MAX_ELEMENTS || model.edges().size() > ArchiMateSchema.MAX_RELATIONSHIPS) {
            throw new IllegalArgumentException("Model exceeds bounded exchange profile");
        }
        Set<String> allIds = model.nodes().stream().map(DiagramNode::id).collect(Collectors.toSet());
        for (DiagramEdge edge : model.edges()) {
            if (!allIds.contains(edge.sourceId()) || !allIds.contains(edge.targetId())) {
                throw new IllegalArgumentException("Unresolved relationship endpoint: " + edge.id());
            }
        }
        for (DiagramNode node : model.nodes()) {
            if (node.parentId() != null && (!allIds.contains(node.parentId()) || node.id().equals(node.parentId()))) {
                throw new IllegalArgumentException("Invalid node parent: " + node.id());
            }
        }
        // Collect non-container node IDs — container-only nodes are visual grouping
        // constructs and must not appear as ArchiMate elements or relationship endpoints.
        Set<String> elementIds = model.nodes().stream()
                .filter(n -> !n.container())
                .map(DiagramNode::id)
                .collect(Collectors.toSet());

        List<ArchiMateLoss> losses = new ArrayList<>();
        List<ArchiMateElement> elements = buildElements(model.nodes(), losses);
        List<ArchiMateRelationship> relationships = buildRelationships(model.edges(), elementIds, losses);
        Map<String, List<String>> organizations = buildOrganizations(model.nodes());
        ArchiMateView view = buildView(model, elementIds);
        Map<String, ArchiMateProperty> properties = new LinkedHashMap<>();
        properties.put("taxonomy.mappingProfile", ArchiMateProperty.text(ArchiMateExchangeProfile.VERSION));
        properties.put("taxonomy.exporterVersion", ArchiMateProperty.text("archimate-writer-v2"));
        properties.put("taxonomy.idProfile", ArchiMateProperty.text("length-prefixed-utf8-hex-v1"));
        properties.put("taxonomy.scoreSemantics", ArchiMateProperty.text("relevance in [0,1]; anchor and impact are separate selection flags"));
        if (model.layout() != null) {
            properties.put("taxonomy.layoutDirection", ArchiMateProperty.text(model.layout().direction()));
            properties.put("taxonomy.groupByLayer", ArchiMateProperty.flag(model.layout().groupByLayer()));
        }
        List<ArchiMateView> views = new ArrayList<>();
        views.add(view);
        Set<String> impact = model.nodes().stream().filter(n -> n.anchor() || n.selectedForImpact())
                .map(DiagramNode::id).filter(elementIds::contains).collect(Collectors.toSet());
        if (!impact.isEmpty() && impact.size() < elementIds.size()) {
            views.add(new ArchiMateView("impact", "Anchor and impact selection",
                    view.nodes().stream().filter(n -> impact.contains(n.elementId())).toList(),
                    view.connections().stream().filter(c -> impact.contains(c.sourceNodeId())
                            && impact.contains(c.targetNodeId())).toList()));
        }
        // Unbound model identity describes typed membership and relationship topology,
        // independent of display text, scores, ordering and generated layout.
        String identity;
        try {
            var digest = java.security.MessageDigest.getInstance("SHA-256");
            elements.stream().sorted(Comparator.comparing(ArchiMateElement::id)).forEach(element -> digest.update(
                    ArchiMateIds.id("element", element.id(), element.properties().get("taxonomy.type").value())
                            .getBytes(java.nio.charset.StandardCharsets.UTF_8)));
            relationships.stream().sorted(Comparator.comparing(ArchiMateRelationship::id)).forEach(relation -> digest.update(
                    ArchiMateIds.id("relationship", relation.id(), relation.sourceId(), relation.targetId(),
                            relation.properties().get("taxonomy.type").value()).getBytes(java.nio.charset.StandardCharsets.UTF_8)));
            identity = "graph-" + java.util.HexFormat.of().formatHex(digest.digest());
        } catch (java.security.NoSuchAlgorithmException exception) {
            throw new IllegalStateException(exception);
        }
        return new ArchiMateModel(identity,
                model.title() != null ? model.title() : "Architecture View", elements,
                relationships, organizations, views, properties, losses);
    }

    private List<ArchiMateElement> buildElements(List<DiagramNode> nodes, List<ArchiMateLoss> losses) {
        List<ArchiMateElement> elements = new ArrayList<>();
        Set<String> identities = new java.util.HashSet<>();
        for (DiagramNode node : nodes) {
            ArchiMateIds.requireIdentity(node.id());
            if (!identities.add(node.id())) throw new IllegalArgumentException("Duplicate node identity: " + node.id());
            if (node.container()) {
                losses.add(new ArchiMateLoss("element", node.id(), "container", "OMITTED",
                        "Visual-only container is not an architecture element; grouping geometry is omitted."));
                continue;
            }
            var mapping = ArchiMateExchangeProfile.element(node.type());
            Map<String, ArchiMateProperty> properties = new LinkedHashMap<>();
            properties.put("taxonomy.type", ArchiMateProperty.text(node.type()));
            properties.put("taxonomy.relevance", score(node.relevance()));
            properties.put("taxonomy.anchor", ArchiMateProperty.flag(node.anchor()));
            properties.put("taxonomy.selectedForImpact", ArchiMateProperty.flag(node.selectedForImpact()));
            properties.put("taxonomy.layer", ArchiMateProperty.number(node.layer()));
            properties.put("taxonomy.depth", ArchiMateProperty.number(node.depth()));
            if (node.parentId() != null) properties.put("taxonomy.parentId", ArchiMateProperty.text(node.parentId()));
            losses.add(new ArchiMateLoss("element", node.id(), "type", mapping.kind(), mapping.rationale()));
            elements.add(new ArchiMateElement(node.id(), node.label(), mapping.targetType(),
                    null, properties));
        }
        return elements;
    }

    private List<ArchiMateRelationship> buildRelationships(List<DiagramEdge> edges,
                                                           Set<String> elementIds, List<ArchiMateLoss> losses) {
        List<ArchiMateRelationship> relationships = new ArrayList<>();
        Set<String> identities = new java.util.HashSet<>();
        for (DiagramEdge edge : edges) {
            ArchiMateIds.requireIdentity(edge.id());
            if (!identities.add(edge.id())) throw new IllegalArgumentException("Duplicate relationship identity: " + edge.id());
            if (!elementIds.contains(edge.sourceId()) || !elementIds.contains(edge.targetId())) {
                losses.add(new ArchiMateLoss("relationship", edge.id(), "endpoints", "OMITTED",
                        "Relationship touches an omitted visual container."));
                continue;
            }
            var mapping = ArchiMateExchangeProfile.relationship(edge.relationType());
            Map<String, ArchiMateProperty> properties = new LinkedHashMap<>();
            properties.put("taxonomy.type", ArchiMateProperty.text(edge.relationType()));
            properties.put("taxonomy.relevance", score(edge.relevance()));
            if (edge.relationCategory() != null) properties.put("taxonomy.relationCategory", ArchiMateProperty.text(edge.relationCategory()));
            losses.add(new ArchiMateLoss("relationship", edge.id(), "type", mapping.kind(), mapping.rationale()));
            relationships.add(new ArchiMateRelationship(edge.id(), edge.sourceId(), edge.targetId(),
                    mapping.targetType(), mapping.accessType(), edge.relationType(), properties));
        }
        return relationships;
    }

    private static ArchiMateProperty score(double value) {
        if (!Double.isFinite(value) || value < 0 || value > 1) throw new IllegalArgumentException("Relevance must be in [0,1]");
        return ArchiMateProperty.number(value);
    }

    private Map<String, List<String>> buildOrganizations(List<DiagramNode> nodes) {
        Map<String, List<String>> organizations = new LinkedHashMap<>();
        for (DiagramNode node : nodes) {
            if (node.container()) continue; // grouping-only nodes are not architecture elements
            String type = node.type() != null ? node.type() : "Unknown";
            organizations.computeIfAbsent(type, k -> new ArrayList<>()).add(node.id());
        }
        return organizations;
    }

    private ArchiMateView buildView(DiagramModel model, Set<String> elementIds) {
        // Group nodes by layer for left-to-right layout
        Map<Integer, List<DiagramNode>> layerGroups = new LinkedHashMap<>();
        for (DiagramNode node : model.nodes()) {
            layerGroups.computeIfAbsent(node.layer(), k -> new ArrayList<>()).add(node);
        }

        List<ArchiMateViewNode> viewNodes = new ArrayList<>();
        int layerIdx = 0;
        for (var entry : layerGroups.entrySet().stream()
                .sorted(Comparator.comparingInt(Map.Entry::getKey)).toList()) {
            List<DiagramNode> layerNodes = entry.getValue();
            for (int i = 0; i < layerNodes.size(); i++) {
                DiagramNode node = layerNodes.get(i);
                if (node.container()) continue; // visual-only container — not an ArchiMate element
                int x = MARGIN_X + layerIdx * H_GAP;
                int y = MARGIN_Y + i * V_GAP;
                int[] color = toColor(node.type());
                int lineWidth = node.anchor() ? 3 : 1;
                viewNodes.add(new ArchiMateViewNode(
                        node.id(),
                        node.id(),
                        x, y,
                        NODE_WIDTH, NODE_HEIGHT,
                        node.label(),
                        color[0], color[1], color[2],
                        lineWidth));
            }
            layerIdx++;
        }

        Set<String> nodeIdSet = elementIds;
        List<ArchiMateViewConnection> viewConnections = new ArrayList<>();
        for (DiagramEdge edge : model.edges()) {
            if (nodeIdSet.contains(edge.sourceId()) && nodeIdSet.contains(edge.targetId())) {
                viewConnections.add(new ArchiMateViewConnection(
                        edge.id(),
                        edge.id(),
                        edge.sourceId(),
                        edge.targetId()));
            }
        }

        String viewName = model.title() != null ? model.title() : "Architecture View";
        return new ArchiMateView("layered", viewName, viewNodes, viewConnections);
    }

    // ── Type mappings ─────────────────────────────────────────────────────────

    public static String toArchiMateType(String taxonomyType) {
        return ArchiMateExchangeProfile.element(taxonomyType).targetType();
    }

    public static String toArchiMateRelType(String relationType) {
        return ArchiMateExchangeProfile.relationship(relationType).targetType();
    }

    public static String toAccessType(String relationType) {
        return ArchiMateExchangeProfile.relationship(relationType).accessType();
    }

    public static int[] toColor(String taxonomyType) {
        if (taxonomyType == null) return new int[]{224, 224, 224};
        return switch (taxonomyType) {
            case "Capabilities"           -> new int[]{255, 255, 181};
            case "Business Processes"     -> new int[]{255, 255, 181};
            case "Business Roles"         -> new int[]{255, 224, 181};
            case "Services"               -> new int[]{181, 255, 255};
            case "COI Services"           -> new int[]{181, 255, 255};
            case "Core Services"          -> new int[]{181, 255, 255};
            case "Communications Services"-> new int[]{204, 224, 255};
            case "Applications"           -> new int[]{181, 255, 255};
            case "User Applications"      -> new int[]{181, 255, 255};
            case "Information Products"   -> new int[]{204, 255, 204};
            case "Systems"                -> new int[]{181, 204, 255};
            case "Components"             -> new int[]{204, 181, 255};
            case "System"                 -> new int[]{204, 204, 255};
            case "Component"              -> new int[]{224, 204, 255};
            default                       -> new int[]{224, 224, 224};
        };
    }
}