"""Writer for ArchiMate Exchange format (.archimate).
Exports pyArchimate models and views to exchange-compatible .archimate files.
Supports ArchiMate 3.x specification and standard exchange metadata.
"""
# ruff: noqa: N999 # legacy module name preserved for API compatibility
import os
import sys
from collections import defaultdict
from collections.abc import Callable
from typing import Any
from typing import cast as _cast
from lxml import etree as et
from lxml.etree import _Element
try:
from ..constants import (
ARCHI_CATEGORY as archi_category, # noqa: N811 # alias matches public API export and fallback import
)
from ..constants import (
DEFAULT_THEME as default_theme, # noqa: N811 # alias matches public API export and fallback import
)
from ..constants import RGBA
from ..enums import ArchiType
from ..helpers.logging import log
from ..model import Model, default_color
from ..view import Node
except ImportError:
sys.path.insert(0, "..")
from pyArchimate import ( # type: ignore[no-redef,attr-defined] # noqa: E401
RGBA,
ArchiType,
Model,
Node,
archi_category,
default_color,
default_theme,
log,
)
__mod__ = __name__.split(".")[len(__name__.split(".")) - 1]
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
_NS_ITEM = "ns:item"
_GetPropId = Callable[[str], str]
def _get_prop_def_id(model: Model, k: str) -> str:
id_list = [x for x, y in model.pdefs.items() if y == k]
if len(id_list) == 0:
prop_id: str = "propid-" + str(len(model.pdefs) + 1)
model.pdefs[prop_id] = k
return prop_id
return str(id_list[0])
def _write_properties(parent: _Element, props: dict[str, object], model: Model) -> None:
pp = et.SubElement(parent, "properties")
for k, v in props.items():
prop_id = _get_prop_def_id(model, k)
p = et.SubElement(pp, "property", propertyDefinitionRef=prop_id)
pv = et.SubElement(p, "value")
pv.text = str(v)
def _write_elem_name_doc(elem: _Element, e: Any) -> None:
if e.name is None:
e.name = e.type
if e.name is not None:
e_name = et.SubElement(elem, "name")
e_name.text = e.name
if e.desc is not None and e.desc != "":
e_desc = et.SubElement(elem, "documentation")
e_desc.text = e.desc
def _write_elem_viewpoints(elem: _Element, e: Any, model: Model) -> None:
for slug in getattr(e, "viewpoints", []):
vp_prop_id = _get_prop_def_id(model, "viewpoint")
pp = elem.find("properties")
if pp is None:
pp = et.SubElement(elem, "properties")
p = et.SubElement(pp, "property", propertyDefinitionRef=vp_prop_id)
pv = et.SubElement(p, "value")
pv.text = slug
def _write_elem_visual_style(elem: _Element, e: Any, model: Model) -> None:
visual_style = getattr(e, "_visual_style", {})
if not visual_style:
return
pp = elem.find("properties")
if pp is None:
pp = et.SubElement(elem, "properties")
for key in ["fillColor", "lineColor", "lineWidth", "transparency"]:
if key in visual_style:
prop_id = _get_prop_def_id(model, key)
p = et.SubElement(pp, "property", propertyDefinitionRef=prop_id)
pv = et.SubElement(p, "value")
pv.text = str(visual_style[key])
def _write_elem_junction_type(elem: _Element, e: Any, model: Model) -> None:
# Junction semantics are encoded in the element's xsi:type (OrJunction /
# AndJunction) written by _write_elements, so a redundant junctionType
# property is only written when the type is the plain 'Junction' fallback.
junction_type = getattr(e, "junction_type", None)
if junction_type and getattr(e, "type", None) == "Junction":
pp = elem.find("properties")
if pp is None:
pp = et.SubElement(elem, "properties")
prop_id = _get_prop_def_id(model, "junctionType")
p = et.SubElement(pp, "property", propertyDefinitionRef=prop_id)
pv = et.SubElement(p, "value")
pv.text = junction_type
def _get_elem_xsi_type(e: Any) -> str:
if e.type != "Junction":
return e.type # type: ignore[no-any-return]
junction_type = getattr(e, "junction_type", None)
if junction_type == "or":
return "OrJunction"
return "AndJunction"
def _ensure_folder(e: Any) -> None:
if e.folder is None:
cat = archi_category[e.type].split("-")[0]
if cat == "Junction":
cat = "Other"
elif cat == "Physical":
cat = "Technology"
e.folder = "/" + cat
def _write_elements(root: _Element, model: Model, xsi: et.QName) -> None:
elems = et.SubElement(root, "elements")
for e in model.elements:
_ensure_folder(e)
elem_xsi_type = _get_elem_xsi_type(e)
elem_attrs = {"identifier": e.uuid, str(xsi): elem_xsi_type}
parent_uuid = getattr(e, "_parent_uuid", None)
if parent_uuid:
elem_attrs["parentId"] = parent_uuid
elem = et.SubElement(elems, "element", elem_attrs)
_write_elem_name_doc(elem, e)
if e.props:
_write_properties(elem, e.props, model)
_write_elem_visual_style(elem, e, model)
_write_elem_junction_type(elem, e, model)
_write_elem_viewpoints(elem, e, model)
def _write_rel_attrs(elem: _Element, e: Any, model: Model) -> None:
if e.access_type is not None and e.type == ArchiType.Access:
elem.set("accessType", e.access_type)
if e.is_directed is not None and e.type == ArchiType.Association:
elem.set("isDirected", "true")
if e.name is not None:
e_name = et.SubElement(elem, "name")
e_name.text = e.name
if e.desc is not None:
e_desc = et.SubElement(elem, "documentation")
e_desc.text = e.desc
if e.props:
_write_properties(elem, e.props, model)
if e.influence_strength is not None and e.type == ArchiType.Influence:
pp = elem.find("properties")
if pp is None:
pp = et.SubElement(elem, "properties")
prop_id = _get_prop_def_id(model, "influenceStrength")
p = et.SubElement(pp, "property", propertyDefinitionRef=prop_id)
pv = et.SubElement(p, "value")
pv.text = e.influence_strength
def _write_relationships(root: _Element, model: Model, xsi: et.QName) -> None:
rels = et.SubElement(root, "relationships")
for e in model.relationships:
assert e.source is not None and e.target is not None
elem = et.SubElement(
rels,
"relationship",
{"identifier": e.uuid, "source": e.source.uuid, "target": e.target.uuid, str(xsi): e.type},
)
_write_rel_attrs(elem, e, model)
def _collect_orgs_dict(model: Model) -> dict[str, list[str]]:
orgs_dict: dict[str, list[str]] = defaultdict(list)
for e in model.elements:
if e.folder is not None:
orgs_dict[e.folder].append(e.uuid)
for r in model.relationships:
if r.folder is not None:
orgs_dict[r.folder].append(r.uuid)
for v in model.views:
if v.folder is not None:
orgs_dict[v.folder].append(v.uuid)
return orgs_dict
def _write_org_path(orgs: _Element, k: str, orgs_dict: dict[str, list[str]], ns_find: dict[str, str]) -> None:
labels = k.split("/")
item = orgs
for label in labels[1:-1]:
if item.find(_NS_ITEM, ns_find) is None:
item = et.SubElement(item, "item")
else:
item = _cast(_Element, item.find(_NS_ITEM, ns_find))
lbl = et.SubElement(item, "label")
lbl.text = label
if item.find(_NS_ITEM, ns_find) is None:
item = et.SubElement(item, "item")
else:
item = _cast(_Element, item.find(_NS_ITEM, ns_find))
lbl = et.SubElement(item, "label")
lbl.text = labels[-1]
for i in orgs_dict[k]:
et.SubElement(item, "item", identifierRef=i)
def _write_organizations(root: _Element, model: Model, ns_find: dict[str, str]) -> None:
orgs_dict = _collect_orgs_dict(model)
orgs = et.SubElement(root, "organizations")
for k in sorted(orgs_dict.keys()):
_write_org_path(orgs, k, orgs_dict, ns_find)
def _write_node_style(n_elem: _Element, n: Node) -> None:
style = et.SubElement(n_elem, "style")
if n.line_color is not None:
lc = et.SubElement(style, "lineColor")
rgb = RGBA()
rgb.color = n.line_color
lc.set("r", str(rgb.r))
lc.set("g", str(rgb.r))
lc.set("b", str(rgb.b))
lc.set("a", "100" if n.opacity is None else str(n.lc_opacity))
if n.fill_color is not None:
if n.fill_color != default_color(n.type or "", default_theme):
fc = et.SubElement(style, "fillColor")
rgb = RGBA()
rgb.color = n.fill_color
fc.set("r", str(rgb.r))
fc.set("g", str(rgb.g))
fc.set("b", str(rgb.b))
fc.set("a", "100" if n.opacity is None else str(n.opacity))
if n.font_name is not None:
ft = et.SubElement(style, "font", attrib={"name": n.font_name, "size": str(n.font_size)})
rgb = RGBA()
ftc = et.SubElement(ft, "color")
rgb.color = n.font_color
ftc.set("r", str(rgb.r))
ftc.set("g", str(rgb.g))
ftc.set("b", str(rgb.b))
def _add_node(parent: _Element, n: Node, xsi: et.QName) -> None:
if n.cat == "Element":
n_elem = et.SubElement(
parent,
"node",
attrib={
"identifier": n.uuid,
"elementRef": n.ref or "",
str(xsi): n.cat,
"x": str(n.x),
"y": str(n.y),
"w": str(n.w),
"h": str(n.h),
},
)
else:
n_elem = et.SubElement(
parent,
"node",
attrib={"identifier": n.uuid, str(xsi): n.cat, "x": str(n.x), "y": str(n.y), "w": str(n.w), "h": str(n.h)},
)
lbl = et.SubElement(n_elem, "label")
lbl.text = n.label
_write_node_style(n_elem, n)
if n.cat == "Model":
et.SubElement(n_elem, "viewRef", ref=n.ref or "")
n_elem.set(str(xsi), "Label")
for sub_n in n.nodes:
_add_node(n_elem, sub_n, xsi)
def _write_conn_style(c_elem: _Element, c: Any) -> None:
style = et.SubElement(c_elem, "style")
if c.line_width is not None:
style.set("lineWidth", str(c.line_width))
if c.line_color is not None:
if c.line_color != default_color(c.type, default_theme):
lc = et.SubElement(style, "lineColor")
rgb = RGBA()
rgb.color = c.line_color
lc.set("r", str(rgb.r))
lc.set("g", str(rgb.g))
lc.set("b", str(rgb.b))
if c.font_name is not None:
ft = et.SubElement(style, "font", attrib={"name": c.font_name, "size": str(c.font_size)})
rgb = RGBA()
ftc = et.SubElement(ft, "color")
rgb.color = c.font_color
ftc.set("r", str(rgb.r))
ftc.set("g", str(rgb.g))
ftc.set("b", str(rgb.b))
def _write_connections(view_elem: _Element, _v: object, xsi: et.QName) -> None:
for c in _v.conns: # type: ignore[attr-defined]
if c.source is None or c.target is None:
log.debug(f"Skipping connection {c.uuid}: missing source or target node")
continue
# Do NOT skip connections between embedded nodes â the OpenGroup format
# must preserve all explicit connections regardless of visual containment.
c_elem = et.SubElement(
view_elem,
"connection",
attrib={
"identifier": c.uuid,
"relationshipRef": c.ref,
str(xsi): "Relationship",
"source": c.source.uuid,
"target": c.target.uuid,
},
)
_write_conn_style(c_elem, c)
for bp in c.get_all_bendpoints():
et.SubElement(c_elem, "bendpoint", x=str(int(round(bp.x))), y=str(int(round(bp.y))))
def _write_views(root: _Element, model: Model, xsi: et.QName) -> None:
if not model.views:
return
views = et.SubElement(root, "views")
diag = et.SubElement(views, "diagrams")
for _v in model.views:
view_attrib: dict[str, str] = {"identifier": _v.uuid, str(xsi): "Diagram"}
# Write primary viewpoint as XML attribute on the view element
primary_vp = getattr(_v, "primary_viewpoint", None)
if primary_vp is not None:
view_attrib["viewpoint"] = primary_vp
view_elem = et.SubElement(diag, "view", attrib=view_attrib)
if _v.name is not None:
v_name = et.SubElement(view_elem, "name")
v_name.text = _v.name
if _v.desc is not None:
doc = et.SubElement(view_elem, "documentation")
doc.text = _v.desc
if _v.props:
_write_properties(view_elem, _v.props, model)
for _n in _v.nodes:
_add_node(view_elem, _n, xsi)
_write_connections(view_elem, _v, xsi)
[docs]
def archimate_writer(model: Model, file_path: str | None = None) -> str:
"""
Method to generate an Archimate XML Open Exchange File format structure as a string object
Used by Model.write(filepath) method
"""
xml = b"""<?xml version="1.0" encoding="UTF-8"?>
<model xmlns="http://www.opengroup.org/xsd/archimate/3.0/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengroup.org/xsd/archimate/3.0/ http://www.opengroup.org/xsd/archimate/3.0/archimate3.xsd" identifier="id-a84d2455d48c44a2847b3407e270599f">
</model>
"""
root = et.fromstring(xml)
nsp_url = "http://www.opengroup.org/xsd/archimate/3.0/" # NOSONAR
xsi_url = "http://www.w3.org/2001/XMLSchema-instance" # NOSONAR
xsi = et.QName(xsi_url, "type")
ns_find: dict[str, str] = {"ns": nsp_url}
name = et.SubElement(root, "name")
name.text = model.name if model.name is not None else "Archimate Model"
if model.desc is not None:
doc = et.SubElement(root, "documentation")
doc.text = model.desc
if model.props:
_write_properties(root, model.props, model)
_write_elements(root, model, xsi)
_write_relationships(root, model, xsi)
_write_organizations(root, model, ns_find)
pd = et.SubElement(root, "propertyDefinitions")
for k, v in model.pdefs.items():
p = et.SubElement(pd, "propertyDefinition", identifier=k, type="string")
p_name = et.SubElement(p, "name")
p_name.text = str(v)
_write_views(root, model, xsi)
pd_check = root.find("propertyDefinitions")
if pd_check is not None and pd_check.find("propertyDefinition") is None:
root.remove(pd_check)
xml_str = et.tostring(root, encoding="UTF-8", pretty_print=True)
if file_path is not None:
try:
with open(file_path, "wb") as fd:
fd.write(xml_str)
except OSError:
log.error(f'{__mod__}.write: Cannot write to file "{file_path}')
return xml_str.decode()