ArchiMateExchangeReader.java
package com.taxonomy.archimate.exchange;
import com.taxonomy.archimate.*;
import com.taxonomy.diagram.*;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import javax.xml.XMLConstants;
import java.util.*;
/** Standards-aware, identity-preserving reader for the declared Taxonomy exchange profile. */
public final class ArchiMateExchangeReader {
public ArchiMateModel read(byte[] xml) {
return read(ArchiMateSchema.parse(xml));
}
public ArchiMateModel read(Document document) {
Element root = document.getDocumentElement();
requireSupportedStructure(root);
Map<String, Definition> definitions = definitions(root);
Map<String, ArchiMateProperty> modelProperties = properties(root, definitions);
if (!ArchiMateExchangeProfile.VERSION.equals(value(modelProperties, "taxonomy.mappingProfile"))) {
throw new IllegalArgumentException("Unsupported Taxonomy ArchiMate exchange profile");
}
String modelId = identity(root, "model", modelProperties);
modelProperties.remove("taxonomy.id");
List<ArchiMateLoss> losses = losses(modelProperties);
List<ArchiMateElement> elements = new ArrayList<>();
for (Element element : children(child(root, "elements"), "element")) {
Map<String, ArchiMateProperty> props = properties(element, definitions);
String id = identity(element, "element", props);
props.remove("taxonomy.id");
var mapping = ArchiMateExchangeProfile.element(value(props, "taxonomy.type"));
String type = type(element);
if (!mapping.targetType().equals(type)) throw new IllegalArgumentException("Element type conflicts with mapping profile: " + id);
elements.add(new ArchiMateElement(id, content(element, "name"), type,
content(element, "documentation"), props));
}
List<ArchiMateRelationship> relationships = new ArrayList<>();
for (Element relation : children(child(root, "relationships"), "relationship")) {
Map<String, ArchiMateProperty> props = properties(relation, definitions);
String id = identity(relation, "relationship", props);
props.remove("taxonomy.id");
var mapping = ArchiMateExchangeProfile.relationship(value(props, "taxonomy.type"));
String accessType = relation.hasAttribute("accessType") ? relation.getAttribute("accessType") : null;
// Schema validation materializes the XSD default. Unqualified Access
// means the same thing whether a consumer writes that default or omits it.
if (mapping.accessType() == null && "Access".equals(accessType)) accessType = null;
String type = type(relation);
if (!mapping.targetType().equals(type) || !Objects.equals(mapping.accessType(), accessType)) {
throw new IllegalArgumentException("Relationship qualifier conflicts with mapping profile: " + id);
}
relationships.add(new ArchiMateRelationship(id, decode("element", relation.getAttribute("source")),
decode("element", relation.getAttribute("target")), type, accessType, content(relation, "name"), props));
}
Map<String, List<String>> organizations = new TreeMap<>();
for (Element item : children(child(root, "organizations"), "item")) {
String label = content(item, "label");
List<String> ids = children(item, "item").stream()
.map(e -> decode("element", e.getAttribute("identifierRef"))).toList();
if (label == null || organizations.putIfAbsent(label, ids) != null) {
throw new IllegalArgumentException("Unsupported or duplicate organization group");
}
}
List<ArchiMateView> views = new ArrayList<>();
for (Element view : children(child(child(root, "views"), "diagrams"), "view")) {
Map<String, ArchiMateProperty> viewProperties = properties(view, definitions);
String viewId = identity(view, "view", viewProperties);
viewProperties.remove("taxonomy.id");
if (!viewProperties.isEmpty()) {
throw new IllegalArgumentException("Unsupported view properties in " + viewId + ": " + viewProperties.keySet());
}
List<ArchiMateViewNode> nodes = new ArrayList<>();
for (Element node : children(view, "node")) {
if (!"Element".equals(type(node))) throw new IllegalArgumentException("Unsupported diagram node type");
Element style = child(node, "style");
Element color = child(style, "fillColor");
if (color == null) throw new IllegalArgumentException("Missing profile node style");
nodes.add(new ArchiMateViewNode(viewIdentity("node", node, viewId),
decode("element", node.getAttribute("elementRef")), integer(node, "x"), integer(node, "y"),
integer(node, "w"), integer(node, "h"), content(node, "label"), integer(color, "r"),
integer(color, "g"), integer(color, "b"), style.hasAttribute("lineWidth") ? integer(style, "lineWidth") : 1));
}
List<ArchiMateViewConnection> connections = new ArrayList<>();
for (Element connection : children(view, "connection")) {
if (!"Relationship".equals(type(connection))) throw new IllegalArgumentException("Unsupported connection type");
connections.add(new ArchiMateViewConnection(viewIdentity("connection", connection, viewId),
decode("relationship", connection.getAttribute("relationshipRef")),
viewReference("node", connection.getAttribute("source"), viewId),
viewReference("node", connection.getAttribute("target"), viewId)));
}
views.add(new ArchiMateView(viewId, content(view, "name"), nodes, connections));
}
ArchiMateModel model = new ArchiMateModel(modelId, content(root, "name"), elements, relationships,
organizations, views, modelProperties, losses);
ArchiMateXmlExporter.validateReferences(model);
toDiagram(model); // validate required property types and values before handing off to application code
return model;
}
public DiagramModel toDiagram(ArchiMateModel model) {
List<DiagramNode> nodes = model.elements().stream().map(element -> {
var p = element.properties();
return new DiagramNode(element.id(), element.label(), value(p, "taxonomy.type"),
number(p, "taxonomy.relevance"), flag(p, "taxonomy.anchor"), integer(p, "taxonomy.layer"),
integer(p, "taxonomy.depth"), flag(p, "taxonomy.selectedForImpact"),
optional(p, "taxonomy.parentId"), false);
}).toList();
List<DiagramEdge> edges = model.relationships().stream().map(relation -> {
var p = relation.properties();
return new DiagramEdge(relation.id(), relation.sourceId(), relation.targetId(),
value(p, "taxonomy.type"), number(p, "taxonomy.relevance"), optional(p, "taxonomy.relationCategory"));
}).toList();
DiagramLayout layout = model.properties().containsKey("taxonomy.layoutDirection")
? new DiagramLayout(value(model.properties(), "taxonomy.layoutDirection"), flag(model.properties(), "taxonomy.groupByLayer"))
: null;
return new DiagramModel(model.title(), nodes, edges, layout);
}
public static boolean hasTaxonomyProfile(Document document) {
Element root = document.getDocumentElement();
Set<String> profileDefinitions = new HashSet<>();
for (Element definition : children(child(root, "propertyDefinitions"), "propertyDefinition")) {
if ("taxonomy.mappingProfile".equals(content(definition, "name"))) {
profileDefinitions.add(definition.getAttribute("identifier"));
}
}
return children(child(root, "properties"), "property").stream()
.anyMatch(property -> profileDefinitions.contains(property.getAttribute("propertyDefinitionRef")));
}
private static String viewIdentity(String kind, Element element, String viewId) {
return viewReference(kind, element.getAttribute("identifier"), viewId);
}
private static String viewReference(String kind, String identifier, String viewId) {
List<String> parts = ArchiMateIds.decode(kind, identifier);
if (parts.size() != 2 || !parts.getFirst().equals(viewId)) throw new IllegalArgumentException("Cross-view identity reference");
return parts.get(1);
}
private static String decode(String kind, String identifier) {
List<String> parts = ArchiMateIds.decode(kind, identifier);
if (parts.size() != 1) throw new IllegalArgumentException("Invalid identity arity");
return parts.getFirst();
}
private static String identity(Element element, String kind, Map<String, ArchiMateProperty> props) {
String decoded = decode(kind, element.getAttribute("identifier"));
if (!decoded.equals(value(props, "taxonomy.id"))) throw new IllegalArgumentException("Conflicting original identity");
return decoded;
}
private static Map<String, Definition> definitions(Element root) {
Map<String, Definition> result = new LinkedHashMap<>();
Set<String> names = new HashSet<>();
for (Element definition : children(child(root, "propertyDefinitions"), "propertyDefinition")) {
String name = content(definition, "name");
if (name == null || !names.add(name)) throw new IllegalArgumentException("Duplicate or absent property name");
if (result.putIfAbsent(definition.getAttribute("identifier"), new Definition(name, definition.getAttribute("type"))) != null) {
throw new IllegalArgumentException("Duplicate property definition");
}
}
return result;
}
private static Map<String, ArchiMateProperty> properties(Element parent, Map<String, Definition> definitions) {
Map<String, ArchiMateProperty> result = new TreeMap<>();
for (Element property : children(child(parent, "properties"), "property")) {
Definition definition = definitions.get(property.getAttribute("propertyDefinitionRef"));
List<Element> values = children(property, "value");
if (definition == null || values.size() != 1) throw new IllegalArgumentException("Unsupported property cardinality or reference");
if (result.putIfAbsent(definition.name(), new ArchiMateProperty(definition.type(), values.getFirst().getTextContent())) != null) {
throw new IllegalArgumentException("Duplicate exchange property: " + definition.name());
}
}
return result;
}
private static List<ArchiMateLoss> losses(Map<String, ArchiMateProperty> props) {
List<ArchiMateLoss> losses = new ArrayList<>();
for (int index = 0; props.containsKey("taxonomy.loss." + index + ".scope"); index++) {
String prefix = "taxonomy.loss." + index + ".";
losses.add(new ArchiMateLoss(value(props, prefix + "scope"), value(props, prefix + "id"),
value(props, prefix + "field"), value(props, prefix + "kind"), value(props, prefix + "rationale")));
for (String field : List.of("scope", "id", "field", "kind", "rationale")) props.remove(prefix + field);
}
if (props.keySet().stream().anyMatch(key -> key.startsWith("taxonomy.loss."))) {
throw new IllegalArgumentException("Incomplete or non-contiguous loss report");
}
return losses;
}
private static String value(Map<String, ArchiMateProperty> properties, String key) {
return typed(properties, key, "string");
}
private static String optional(Map<String, ArchiMateProperty> properties, String key) {
return properties.containsKey(key) ? value(properties, key) : null;
}
private static String typed(Map<String, ArchiMateProperty> properties, String key, String type) {
ArchiMateProperty property = properties.get(key);
if (property == null || !type.equals(property.type())) throw new IllegalArgumentException("Missing or wrongly typed property: " + key);
return property.value();
}
private static double number(Map<String, ArchiMateProperty> properties, String key) {
double result = Double.parseDouble(typed(properties, key, "number"));
if (!Double.isFinite(result) || result < 0 || result > 1) throw new IllegalArgumentException("Invalid relevance property");
return result;
}
private static int integer(Map<String, ArchiMateProperty> properties, String key) {
return new java.math.BigDecimal(typed(properties, key, "number")).intValueExact();
}
private static boolean flag(Map<String, ArchiMateProperty> properties, String key) {
return Boolean.parseBoolean(typed(properties, key, "boolean"));
}
private static int integer(Element element, String attribute) {
return Integer.parseInt(element.getAttribute(attribute));
}
public static String type(Element element) {
String value = element.getAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "type");
int colon = value.indexOf(':');
String prefix = colon < 0 ? null : value.substring(0, colon);
if (!ArchiMateSchema.NAMESPACE.equals(element.lookupNamespaceURI(prefix))) {
throw new IllegalArgumentException("Unexpected ArchiMate type namespace");
}
return colon < 0 ? value : value.substring(colon + 1);
}
public static String content(Element parent, String name) {
Element child = child(parent, name);
return child == null ? null : child.getTextContent();
}
public static Element child(Element parent, String name) {
List<Element> matches = children(parent, name);
return matches.isEmpty() ? null : matches.getFirst();
}
public static List<Element> children(Element parent, String name) {
if (parent == null) return List.of();
List<Element> result = new ArrayList<>();
for (Node node = parent.getFirstChild(); node != null; node = node.getNextSibling()) {
if (node instanceof Element element && ArchiMateSchema.NAMESPACE.equals(element.getNamespaceURI())
&& name.equals(element.getLocalName())) result.add(element);
}
return result;
}
/** Fail closed on schema-valid constructs that this profile cannot reconstruct. */
private static void requireSupportedStructure(Element element) {
String name = element.getLocalName();
if (element.hasAttributeNS(XMLConstants.XML_NS_URI, "lang")
&& !"en".equals(element.getAttributeNS(XMLConstants.XML_NS_URI, "lang"))) {
throw new IllegalArgumentException("Unsupported language-tag projection");
}
if ("view".equals(name) && element.hasAttribute("viewpoint")
&& !"Layered".equals(element.getAttribute("viewpoint"))) {
throw new IllegalArgumentException("Unsupported viewpoint projection");
}
Set<String> allowedChildren = switch (name) {
case "model" -> Set.of("name", "properties", "elements", "relationships", "organizations", "propertyDefinitions", "views");
case "elements" -> Set.of("element");
case "element" -> Set.of("name", "documentation", "properties");
case "relationships" -> Set.of("relationship");
case "relationship" -> Set.of("name", "properties");
case "organizations", "item" -> Set.of("item", "label");
case "propertyDefinitions" -> Set.of("propertyDefinition");
case "propertyDefinition" -> Set.of("name");
case "properties" -> Set.of("property");
case "property" -> Set.of("value");
case "views" -> Set.of("diagrams");
case "diagrams" -> Set.of("view");
case "view" -> Set.of("name", "properties", "node", "connection");
case "node" -> Set.of("label", "style");
case "style" -> Set.of("fillColor");
default -> Set.of();
};
Set<String> repeated = switch (name) {
case "elements" -> Set.of("element");
case "relationships" -> Set.of("relationship");
case "organizations", "item" -> Set.of("item");
case "propertyDefinitions" -> Set.of("propertyDefinition");
case "properties" -> Set.of("property");
case "diagrams" -> Set.of("view");
case "view" -> Set.of("node", "connection");
default -> Set.of();
};
Set<String> seen = new HashSet<>();
for (Node node = element.getFirstChild(); node != null; node = node.getNextSibling()) {
if (!(node instanceof Element child)) continue;
if (!ArchiMateSchema.NAMESPACE.equals(child.getNamespaceURI()) || !allowedChildren.contains(child.getLocalName())
|| !repeated.contains(child.getLocalName()) && !seen.add(child.getLocalName())) {
throw new IllegalArgumentException("Unsupported exchange structure below " + name + ": " + child.getLocalName());
}
requireSupportedStructure(child);
}
Set<String> attributes = switch (name) {
case "model", "element" -> Set.of("identifier");
case "view" -> Set.of("identifier", "viewpoint");
case "propertyDefinition" -> Set.of("identifier", "type");
case "relationship" -> Set.of("identifier", "source", "target", "accessType");
case "node" -> Set.of("identifier", "elementRef", "x", "y", "w", "h");
case "connection" -> Set.of("identifier", "relationshipRef", "source", "target");
case "style" -> Set.of("lineWidth");
case "fillColor" -> Set.of("r", "g", "b");
case "property" -> Set.of("propertyDefinitionRef");
case "item" -> Set.of("identifierRef");
default -> Set.of();
};
for (int i = 0; i < element.getAttributes().getLength(); i++) {
var attribute = (org.w3c.dom.Attr) element.getAttributes().item(i);
// XSD defaults are semantically normalized by the reader (for example opacity and Access).
if (!attribute.getSpecified()) continue;
String namespace = attribute.getNamespaceURI();
if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespace)
|| XMLConstants.XML_NS_URI.equals(namespace) && "lang".equals(attribute.getLocalName())
|| XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI.equals(namespace)
&& Set.of("type", "schemaLocation").contains(attribute.getLocalName())) continue;
if (namespace != null || !attributes.contains(attribute.getName())) {
throw new IllegalArgumentException("Unsupported exchange attribute on " + name + ": " + attribute.getName());
}
}
}
private record Definition(String name, String type) { }
}