ArchiMateXmlExporter.java
package com.taxonomy.archimate.exchange;
import com.taxonomy.archimate.*;
import javax.xml.XMLConstants;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;
/** Typed ArchiMate 3.1 subset writer. Every result passes offline normative validation. */
public class ArchiMateXmlExporter {
public byte[] export(ArchiMateModel model) {
validateReferences(model);
try {
ByteArrayOutputStream output = new BoundedOutput();
XMLStreamWriter xml = XMLOutputFactory.newDefaultFactory().createXMLStreamWriter(output, "UTF-8");
try {
write(xml, model);
} finally {
xml.close();
}
byte[] bytes = output.toByteArray();
ArchiMateSchema.parse(bytes);
return bytes;
} catch (XMLStreamException exception) {
throw new IllegalArgumentException("Cannot serialize ArchiMate exchange model", exception);
}
}
private void write(XMLStreamWriter xml, ArchiMateModel model) throws XMLStreamException {
Map<String, ArchiMateProperty> modelProperties = modelProperties(model);
Map<String, String> definitions = new TreeMap<>();
collectDefinitions(definitions, modelProperties);
for (ArchiMateElement element : model.elements()) collectDefinitions(definitions, identity(element.id(), element.properties()));
for (ArchiMateRelationship relation : model.relationships()) collectDefinitions(definitions, identity(relation.id(), relation.properties()));
if (!model.views().isEmpty()) definitions.put("taxonomy.id", "string");
xml.writeStartDocument("UTF-8", "1.0");
xml.writeStartElement("model");
xml.writeDefaultNamespace(ArchiMateSchema.NAMESPACE);
xml.writeNamespace("xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
xml.writeAttribute("xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "schemaLocation",
ArchiMateSchema.NAMESPACE + " http://www.opengroup.org/xsd/archimate/3.1/archimate3_Diagram.xsd");
attr(xml, "identifier", ArchiMateIds.id("model", model.id()));
text(xml, "name", model.title());
properties(xml, modelProperties);
if (!model.elements().isEmpty()) {
xml.writeStartElement("elements");
for (ArchiMateElement element : model.elements()) {
xml.writeStartElement("element");
attr(xml, "identifier", ArchiMateIds.id("element", element.id()));
type(xml, element.archiMateType());
text(xml, "name", element.label());
if (element.documentation() != null && !element.documentation().isEmpty()) text(xml, "documentation", element.documentation());
properties(xml, identity(element.id(), element.properties()));
xml.writeEndElement();
}
xml.writeEndElement();
}
if (!model.relationships().isEmpty()) {
xml.writeStartElement("relationships");
for (ArchiMateRelationship relation : model.relationships()) {
xml.writeStartElement("relationship");
attr(xml, "identifier", ArchiMateIds.id("relationship", relation.id()));
type(xml, relation.archiMateType());
attr(xml, "source", ArchiMateIds.id("element", relation.sourceId()));
attr(xml, "target", ArchiMateIds.id("element", relation.targetId()));
if (relation.accessType() != null) attr(xml, "accessType", relation.accessType());
text(xml, "name", relation.name());
properties(xml, identity(relation.id(), relation.properties()));
xml.writeEndElement();
}
xml.writeEndElement();
}
if (!model.organizations().isEmpty()) {
xml.writeStartElement("organizations");
for (var entry : new TreeMap<>(model.organizations()).entrySet()) {
xml.writeStartElement("item");
text(xml, "label", entry.getKey());
for (String id : entry.getValue()) {
xml.writeEmptyElement("item");
attr(xml, "identifierRef", ArchiMateIds.id("element", id));
}
xml.writeEndElement();
}
xml.writeEndElement();
}
if (!definitions.isEmpty()) {
xml.writeStartElement("propertyDefinitions");
for (var entry : definitions.entrySet()) {
xml.writeStartElement("propertyDefinition");
attr(xml, "identifier", ArchiMateIds.id("property", entry.getKey()));
attr(xml, "type", entry.getValue());
text(xml, "name", entry.getKey());
xml.writeEndElement();
}
xml.writeEndElement();
}
if (!model.views().isEmpty()) {
xml.writeStartElement("views");
xml.writeStartElement("diagrams");
for (ArchiMateView view : model.views()) writeView(xml, view);
xml.writeEndElement();
xml.writeEndElement();
}
xml.writeEndElement();
xml.writeEndDocument();
}
private void writeView(XMLStreamWriter xml, ArchiMateView view) throws XMLStreamException {
// The XSD declares diagrams/view directly as Diagram; xsi:type is not required here.
xml.writeStartElement("view");
attr(xml, "identifier", ArchiMateIds.id("view", view.id()));
attr(xml, "viewpoint", "Layered");
text(xml, "name", view.name());
properties(xml, identity(view.id(), Map.of()));
for (ArchiMateViewNode node : view.nodes()) {
xml.writeStartElement("node");
attr(xml, "identifier", ArchiMateIds.id("node", view.id(), node.id()));
type(xml, "Element");
attr(xml, "elementRef", ArchiMateIds.id("element", node.elementId()));
attr(xml, "x", node.x()); attr(xml, "y", node.y());
attr(xml, "w", node.w()); attr(xml, "h", node.h());
text(xml, "label", node.label());
xml.writeStartElement("style");
if (node.lineWidth() != 1) attr(xml, "lineWidth", node.lineWidth());
xml.writeEmptyElement("fillColor");
attr(xml, "r", node.r()); attr(xml, "g", node.g()); attr(xml, "b", node.b());
xml.writeEndElement();
xml.writeEndElement();
}
for (ArchiMateViewConnection connection : view.connections()) {
xml.writeEmptyElement("connection");
attr(xml, "identifier", ArchiMateIds.id("connection", view.id(), connection.id()));
type(xml, "Relationship");
attr(xml, "relationshipRef", ArchiMateIds.id("relationship", connection.relationshipId()));
attr(xml, "source", ArchiMateIds.id("node", view.id(), connection.sourceNodeId()));
attr(xml, "target", ArchiMateIds.id("node", view.id(), connection.targetNodeId()));
}
xml.writeEndElement();
}
public static Map<String, ArchiMateProperty> modelProperties(ArchiMateModel model) {
Map<String, ArchiMateProperty> result = new TreeMap<>(identity(model.id(), model.properties()));
for (int i = 0; i < model.losses().size(); i++) {
ArchiMateLoss loss = model.losses().get(i);
String prefix = "taxonomy.loss." + i + ".";
result.put(prefix + "scope", ArchiMateProperty.text(loss.scope()));
result.put(prefix + "id", ArchiMateProperty.text(loss.id()));
result.put(prefix + "field", ArchiMateProperty.text(loss.field()));
result.put(prefix + "kind", ArchiMateProperty.text(loss.kind()));
result.put(prefix + "rationale", ArchiMateProperty.text(loss.rationale()));
}
return result;
}
private static Map<String, ArchiMateProperty> identity(String id, Map<String, ArchiMateProperty> properties) {
Map<String, ArchiMateProperty> result = new TreeMap<>(properties);
ArchiMateProperty original = result.put("taxonomy.id", ArchiMateProperty.text(id));
if (original != null && !original.equals(ArchiMateProperty.text(id))) {
throw new IllegalArgumentException("Conflicting Taxonomy identity property");
}
return result;
}
private static void collectDefinitions(Map<String, String> definitions, Map<String, ArchiMateProperty> properties) {
properties.forEach((key, value) -> {
String previous = definitions.putIfAbsent(key, value.type());
if (previous != null && !previous.equals(value.type())) throw new IllegalArgumentException("Conflicting property types: " + key);
});
}
private static void properties(XMLStreamWriter xml, Map<String, ArchiMateProperty> properties) throws XMLStreamException {
if (properties.isEmpty()) return;
xml.writeStartElement("properties");
for (var entry : new TreeMap<>(properties).entrySet()) {
xml.writeStartElement("property");
attr(xml, "propertyDefinitionRef", ArchiMateIds.id("property", entry.getKey()));
text(xml, "value", entry.getValue().value());
xml.writeEndElement();
}
xml.writeEndElement();
}
private static void type(XMLStreamWriter xml, String value) throws XMLStreamException {
xml.writeAttribute("xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "type", ArchiMateIds.requireXmlText(value));
}
private static void attr(XMLStreamWriter xml, String name, Object value) throws XMLStreamException {
xml.writeAttribute(name, ArchiMateIds.requireXmlText(String.valueOf(value)));
}
private static void text(XMLStreamWriter xml, String name, String value) throws XMLStreamException {
xml.writeStartElement(name);
xml.writeAttribute("xml", XMLConstants.XML_NS_URI, "lang", "en");
String[] parts = ArchiMateIds.requireXmlText(value).split("\r", -1);
for (int i = 0; i < parts.length; i++) {
if (i > 0) xml.writeEntityRef("#xD");
xml.writeCharacters(parts[i]);
}
xml.writeEndElement();
}
static void validateReferences(ArchiMateModel model) {
Objects.requireNonNull(model, "model");
ArchiMateIds.requireIdentity(model.id());
if (model.title() == null) throw new IllegalArgumentException("Missing model title");
if (model.elements().size() > ArchiMateSchema.MAX_ELEMENTS
|| model.relationships().size() > ArchiMateSchema.MAX_RELATIONSHIPS || model.views().size() > 32) {
throw new IllegalArgumentException("Model exceeds bounded exchange profile");
}
Map<String, ArchiMateElement> elements = index(model.elements(), ArchiMateElement::id);
if (elements.values().stream().anyMatch(element -> element.label() == null)) {
throw new IllegalArgumentException("Missing element label");
}
Map<String, ArchiMateRelationship> relations = index(model.relationships(), ArchiMateRelationship::id);
for (var relation : relations.values()) {
if (relation.name() == null) throw new IllegalArgumentException("Missing relationship name");
requireRef(elements, relation.sourceId()); requireRef(elements, relation.targetId());
}
model.organizations().values().forEach(ids -> ids.forEach(id -> requireRef(elements, id)));
index(model.views(), ArchiMateView::id);
for (var view : model.views()) {
if (view.name() == null) throw new IllegalArgumentException("Missing view name");
var nodes = index(view.nodes(), ArchiMateViewNode::id);
if (nodes.values().stream().anyMatch(node -> node.label() == null)) throw new IllegalArgumentException("Missing view node label");
index(view.connections(), ArchiMateViewConnection::id);
nodes.values().forEach(node -> requireRef(elements, node.elementId()));
for (var connection : view.connections()) {
var relation = requireRef(relations, connection.relationshipId());
var source = requireRef(nodes, connection.sourceNodeId());
var target = requireRef(nodes, connection.targetNodeId());
if (!relation.sourceId().equals(source.elementId()) || !relation.targetId().equals(target.elementId())) {
throw new IllegalArgumentException("View connection does not follow relationship endpoints: " + connection.id());
}
}
}
}
private static <T> Map<String, T> index(List<T> values, java.util.function.Function<T, String> identity) {
Map<String, T> result = new LinkedHashMap<>();
for (T value : values) {
String id = identity.apply(value);
ArchiMateIds.requireIdentity(id);
if (result.putIfAbsent(id, value) != null) throw new IllegalArgumentException("Duplicate architecture identity: " + id);
}
return result;
}
private static <T> T requireRef(Map<String, T> values, String id) {
T value = values.get(id);
if (value == null) throw new IllegalArgumentException("Unresolved architecture reference: " + id);
return value;
}
static String escapeXml(String text) {
return ArchiMateIds.requireXmlText(text).replace("&", "&").replace("<", "<")
.replace(">", ">").replace("\"", """).replace("'", "'");
}
private static final class BoundedOutput extends ByteArrayOutputStream {
@Override public synchronized void write(int value) {
if (count >= ArchiMateSchema.MAX_BYTES) throw new IllegalArgumentException("Exchange XML exceeds byte limit");
super.write(value);
}
@Override public synchronized void write(byte[] bytes, int offset, int length) {
if (length > ArchiMateSchema.MAX_BYTES - count) throw new IllegalArgumentException("Exchange XML exceeds byte limit");
super.write(bytes, offset, length);
}
}
}