ArchiMateXmlImporter.java
package com.taxonomy.catalog.service;
import com.taxonomy.dto.ArchiMateImportResult;
import com.taxonomy.dsl.mapping.profiles.ArchiMateMappingProfile;
import com.taxonomy.model.RelationType;
import com.taxonomy.catalog.model.TaxonomyNode;
import com.taxonomy.catalog.model.TaxonomyRelation;
import com.taxonomy.catalog.repository.TaxonomyNodeRepository;
import com.taxonomy.catalog.repository.TaxonomyRelationRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamReader;
import java.io.InputStream;
import java.util.*;
import com.taxonomy.export.ArchiMateDiagramService;
/**
* Imports an ArchiMate 3.x Model Exchange Format XML file and maps its
* elements and relationships to taxonomy nodes and relations.
*
* <p>The importer performs the following steps:
* <ol>
* <li>Parse elements from the XML and extract id, type, and label.</li>
* <li>Parse relationships from the XML and extract source, target, and type.</li>
* <li>Map each ArchiMate element type to a taxonomy root code using the
* reverse of {@link ArchiMateDiagramService#toArchiMateType(String)}.</li>
* <li>Match elements to existing taxonomy nodes by name similarity.</li>
* <li>Create new relations for matched elements.</li>
* </ol>
*/
@Service
public class ArchiMateXmlImporter {
private static final Logger log = LoggerFactory.getLogger(ArchiMateXmlImporter.class);
private final TaxonomyNodeRepository nodeRepository;
private final TaxonomyRelationRepository relationRepository;
/** Shared mapping profile for ArchiMate types. */
private static final ArchiMateMappingProfile PROFILE = new ArchiMateMappingProfile();
public ArchiMateXmlImporter(TaxonomyNodeRepository nodeRepository,
TaxonomyRelationRepository relationRepository) {
this.nodeRepository = nodeRepository;
this.relationRepository = relationRepository;
}
/**
* Imports an ArchiMate XML file and creates taxonomy relations.
*
* @param inputStream the XML input stream
* @return the import result with statistics
*/
@Transactional
public ArchiMateImportResult importXml(InputStream inputStream) {
ArchiMateImportResult result = new ArchiMateImportResult();
List<String> notes = new ArrayList<>();
try {
// Parse XML
Map<String, ParsedElement> elements = new LinkedHashMap<>();
List<ParsedRelationship> relationships = new ArrayList<>();
parseXml(inputStream, elements, relationships);
notes.add("Parsed " + elements.size() + " elements and " + relationships.size() + " relationships from XML");
// Match elements to taxonomy nodes
Map<String, TaxonomyNode> matchedNodes = matchElements(elements, notes);
result.setElementsMatched(matchedNodes.size());
result.setElementsUnmatched(elements.size() - matchedNodes.size());
result.setElementsImported(elements.size());
// Create relations for matched element pairs
int relationsCreated = createRelations(relationships, elements, matchedNodes, notes);
result.setRelationsImported(relationsCreated);
} catch (Exception e) {
log.error("ArchiMate import failed", e);
notes.add("Import error: " + e.getMessage());
}
result.setNotes(notes);
return result;
}
private void parseXml(InputStream inputStream,
Map<String, ParsedElement> elements,
List<ParsedRelationship> relationships) throws Exception {
XMLInputFactory factory = XMLInputFactory.newInstance();
// Security: disable external entities
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
XMLStreamReader reader = factory.createXMLStreamReader(inputStream);
while (reader.hasNext()) {
int event = reader.next();
if (event == XMLStreamConstants.START_ELEMENT) {
String localName = reader.getLocalName();
if ("element".equals(localName)) {
parseElement(reader, elements);
} else if ("relationship".equals(localName)) {
parseRelationship(reader, relationships);
}
}
}
reader.close();
}
private void parseElement(XMLStreamReader reader, Map<String, ParsedElement> elements) throws Exception {
String identifier = reader.getAttributeValue(null, "identifier");
String xsiType = reader.getAttributeValue(
"http://www.w3.org/2001/XMLSchema-instance", "type");
if (identifier == null || xsiType == null) return;
// Strip "id-" prefix if present
String id = identifier.startsWith("id-") ? identifier.substring(3) : identifier;
String label = null;
String documentation = null;
// Read child elements for name and documentation
int depth = 1;
while (reader.hasNext() && depth > 0) {
int event = reader.next();
if (event == XMLStreamConstants.START_ELEMENT) {
depth++;
if ("name".equals(reader.getLocalName())) {
label = reader.getElementText();
depth--; // getElementText consumes the end element
} else if ("documentation".equals(reader.getLocalName())) {
documentation = reader.getElementText();
depth--;
}
} else if (event == XMLStreamConstants.END_ELEMENT) {
depth--;
}
}
elements.put(id, new ParsedElement(id, xsiType, label, documentation));
}
private void parseRelationship(XMLStreamReader reader, List<ParsedRelationship> relationships) throws Exception {
String identifier = reader.getAttributeValue(null, "identifier");
String xsiType = reader.getAttributeValue(
"http://www.w3.org/2001/XMLSchema-instance", "type");
String source = reader.getAttributeValue(null, "source");
String target = reader.getAttributeValue(null, "target");
if (identifier == null || source == null || target == null) return;
// Strip "id-" prefix variants
String sourceId = source.startsWith("id-") ? source.substring(3) : source;
String targetId = target.startsWith("id-") ? target.substring(3) : target;
String type = xsiType != null ? xsiType : "Association";
relationships.add(new ParsedRelationship(identifier, sourceId, targetId, type));
}
private Map<String, TaxonomyNode> matchElements(Map<String, ParsedElement> elements,
List<String> notes) {
Map<String, TaxonomyNode> matched = new LinkedHashMap<>();
for (ParsedElement el : elements.values()) {
String taxonomyRoot = PROFILE.mapElementType(el.type);
if (taxonomyRoot == null) {
notes.add("Unknown ArchiMate type: " + el.type + " for element " + el.label);
continue;
}
// Try to find a matching taxonomy node by name in the expected root
if (el.label != null && !el.label.isBlank()) {
List<TaxonomyNode> candidates = nodeRepository
.findByTaxonomyRootOrderByLevelAscNameEnAsc(taxonomyRoot);
TaxonomyNode bestMatch = findBestMatch(el.label, candidates);
if (bestMatch != null) {
matched.put(el.id, bestMatch);
}
}
}
notes.add("Matched " + matched.size() + " of " + elements.size() + " elements to taxonomy nodes");
return matched;
}
private TaxonomyNode findBestMatch(String label, List<TaxonomyNode> candidates) {
if (candidates.isEmpty()) return null;
String normalizedLabel = label.toLowerCase(Locale.ROOT).trim();
// First: exact name match
for (TaxonomyNode node : candidates) {
if (node.getNameEn() != null &&
node.getNameEn().toLowerCase(Locale.ROOT).trim().equals(normalizedLabel)) {
return node;
}
}
// Second: contains match
for (TaxonomyNode node : candidates) {
if (node.getNameEn() != null) {
String nodeName = node.getNameEn().toLowerCase(Locale.ROOT).trim();
if (nodeName.contains(normalizedLabel) || normalizedLabel.contains(nodeName)) {
return node;
}
}
}
return null;
}
private int createRelations(List<ParsedRelationship> relationships,
Map<String, ParsedElement> elements,
Map<String, TaxonomyNode> matchedNodes,
List<String> notes) {
int created = 0;
for (ParsedRelationship rel : relationships) {
TaxonomyNode sourceNode = matchedNodes.get(rel.sourceId);
TaxonomyNode targetNode = matchedNodes.get(rel.targetId);
if (sourceNode == null || targetNode == null) continue;
String relTypeName = PROFILE.mapRelationType(rel.type);
RelationType relationType = relTypeName != null
? RelationType.valueOf(relTypeName)
: RelationType.RELATED_TO;
// Check if relation already exists
List<TaxonomyRelation> existing = relationRepository
.findBySourceNodeCode(sourceNode.getCode());
boolean alreadyExists = existing.stream()
.anyMatch(r -> r.getTargetNode().getCode().equals(targetNode.getCode()) &&
r.getRelationType() == relationType);
if (!alreadyExists) {
TaxonomyRelation newRel = new TaxonomyRelation();
newRel.setSourceNode(sourceNode);
newRel.setTargetNode(targetNode);
newRel.setRelationType(relationType);
newRel.setDescription("Imported from ArchiMate XML");
newRel.setProvenance("ARCHIMATE_IMPORT");
relationRepository.save(newRel);
created++;
}
}
notes.add("Created " + created + " new relations (" +
(relationships.size() - created) + " already existed or had unmatched endpoints)");
return created;
}
// ── Internal record types ─────────────────────────────────────────────────
/** Represents a parsed ArchiMate element with its id, ArchiMate type, display label, and optional documentation text. */
private record ParsedElement(String id, String type, String label, String documentation) {}
/** Represents a parsed ArchiMate relationship linking a source element to a target element via a specific relationship type. */
private record ParsedRelationship(String id, String sourceId, String targetId, String type) {}
}