Skip to content

Instantly share code, notes, and snippets.

@christhearchitect
Last active December 5, 2022 19:19
Show Gist options
  • Save christhearchitect/ee35ab560d2d42773a5a982b5e240452 to your computer and use it in GitHub Desktop.
Save christhearchitect/ee35ab560d2d42773a5a982b5e240452 to your computer and use it in GitHub Desktop.
#jArchi
/*
* This script attemps to import a draw.io (aka diagrams.net) diagram into an Archi model by
* empirically mapping the observable attributes of the XML elements exported from draw.io to Archimate
* elements and relationships. Please note that this mapping is fragile, because draw.io doesn't seem
* to generate the XML attributes in a very consistent way; also, we must unfortunately use element color
* as a key differentiator, to compensate for the lack of usable archimate information in the XML file.
* All of this means that minor changes in the draw.io XML format can break the mapping...
* Notes:
* -- To generate the draw.io file, export it as an *uncompressed* XML file.
* -- When running the script, make sure you have set the current model to import it in (e.g. by selecting a view in the model)
* -- In draw.io, make sure that the relationships you want to import are *really* connected to their source and target elements
* (in Archi, we need both end-elements to create a relationship)
* -- The script generates a view containing the newly imported objects, but this is pretty much experimental
* (the layout for the elements is the same as in draw.io, but the relations just have the default routing provided by Archi)
* -- The current code does very little error/exception handling
*
* NB: all of this is pretty much a work in progress. Possible future improvements:
* -- More test cases!
* -- Better error/exception handling
* -- Create new objects in a dedicated folder ?
* -- ..
*
* Author: christhearchitect
* Version: 0.2
*
*/
console.clear();
console.log("ImportDrawio\n------------\n");
// Feature toggles
var debug = true;
var forceImport = false; // if true, the script will import/create new model objects even if identically named objects already exist (which will create doubles/triples/etc)
var addSourceProp = true; // if true, a "source" property is added to the new model objects, pointing to the source file and cell ID
// Parameters
var importedViewName = "_Imported from draw.io"; // name for the view containing the newly imported objects
debug?console.log("//","Loading libraries"):null;
load(__DIR__ + "lib/utils.ajs");
load(__DIR__ + "lib/from-xml.min.js");
// the fromXML function creates a JSON object tree with the following naming conventions:
// -- the key of an XML tag is the tag name, without adornments
// -- the key of an XML attribute is prefixed with "@"
// -- the key for content (between XML tags) is "#"
var businessColor = "#ffff99";
var applicationColor = "#99ffff";
var technologyColor = "#AFFFAF";
var physicalColor = "#AFFFAF"; // Currently, this is the same as technologyColor
var motivationColor = "#CCCCFF";
var strategyColor = "#F5DEAA";
var implementationColor = "#FFE0E0";
var implementationColorL = "#E0FFE0";
var groupingColor = ""; // Not used at this time...
var locationColor = "#FFB973";
var junctionAndColor = "#000000";
var junctionOrColor = "#ffffff";
// The following tables use the minimum number of attributes needed to distinguish between
// the different draw.io drawing elements and map them to Archi.
// Draw.io format: https://jgraph.github.io/mxgraph/docs/js-api/files/model/mxCell-js.html
// The Archi types are taken from https://github.com/archimatetool/archi-scripting-plugin/wiki/jArchi-Collection
var archiElemMap = [
// tags/shape/appType/
// Archi type archiType/techType fillColor
// --------------------------- ------------------ ----------------
["application-collaboration", "collab", applicationColor],
["application-component", "comp", applicationColor],
["application-event", "event", applicationColor],
["application-function", "func", applicationColor],
["application-interaction", "interaction", applicationColor],
["application-interface", "interface", applicationColor],
["application-process", "proc", applicationColor],
["application-service", "serv", applicationColor],
["artifact", "artifact", technologyColor],
["assessment", "assess", motivationColor],
["business-actor", "actor", businessColor],
["business-collaboration", "collab", businessColor],
["business-event", "event", businessColor],
["business-function", "func", businessColor],
["business-interaction", "interaction", businessColor],
["business-interface", "interface", businessColor],
["business-object", "businessObject", businessColor],
["business-process", "proc", businessColor],
["business-role", "role", businessColor],
["business-service", "serv", businessColor],
// Not (yet) supported:
// ["canvas-model-block", "", ""],
// ["canvas-model-image", "", ""],
// ["canvas-model-sticky" "", ""],
["capability", "capability", strategyColor],
["communication-network", "commNetw", technologyColor],
["communication-network", "netw", technologyColor],
["constraint", "constraint", motivationColor],
["contract", "contract", businessColor],
["course-of-action", "course", strategyColor],
["data-object", "businessObject", applicationColor],
["deliverable", "deliverable", implementationColor],
["device", "device", technologyColor],
// No direct equivalent in draw.io:
// ["diagram-model-connection", "", ""],
// ["diagram-model-group", "", ""],
// ["diagram-model-image", "", ""],
// ["diagram-model-note", "", ""],
// ["diagram-model-reference", "", ""],
["distribution-network", "distribution", physicalColor],
["driver", "driver", motivationColor],
["equipment", "equipment", physicalColor],
["facility", "facility", physicalColor],
["gap", "gap", implementationColorL],
["goal", "goal", motivationColor],
["grouping", "folder", groupingColor],
["implementation-event", "event", implementationColor],
["junction_and", "ellipse", junctionAndColor],
["junction_or", "ellipse", junctionOrColor],
["location", "location", locationColor],
["material", "material", physicalColor],
["meaning", "cloud", motivationColor],
["node", "node", technologyColor],
["outcome", "outcome", motivationColor],
["path", "path", physicalColor],
["plateau", "plateau", implementationColorL],
["principle", "principle", motivationColor],
["product", "product", businessColor],
["representation", "representation", businessColor],
["requirement", "requirement", motivationColor],
["resource", "resource", strategyColor],
// Not (yet) supported:
// ["sketch-model-actor", "", ""],
// ["sketch-model-sticky", "", ""],
["stakeholder", "role", motivationColor],
["system-software", "sysSw", technologyColor],
["technology-collaboration", "collab", technologyColor],
["technology-event", "event", technologyColor],
["technology-function", "func", technologyColor],
["technology-interaction", "interaction", technologyColor],
["technology-interface", "interface", technologyColor],
["technology-process", "proc", technologyColor],
["technology-service", "serv", technologyColor],
["value", "ellipse", motivationColor],
["value-stream", "NOTSUPPORTED", motivationColor], // not supported by draw.io at this time...
["work-package", "rounded", implementationColor]
];
var archiRelMap = [
// Archi type startArrow endArrow endFill dashed dashPattern
// ---------------------------- ---------- -------- ------- ------ -----------
["access-relationship", "", "none", "", "1", "1"],
["access-relationship_read", "open", "", "", "1", "1"],
["access-relationship_write", "", "open", "0", "1", "1"],
["access-relationship_readwrite", "open", "open", "0", "1", "1"],
["aggregation-relationship", "", "diamondThin", "0", "", "" ],
["assignment-relationship", "oval", "block", "1", "", "" ],
["association-relationship", "", "none", "", "", "" ],
["composition-relationship", "", "diamondThin", "1", "", "" ],
["flow-relationship", "", "block", "1", "1", "6"],
["influence-relationship", "", "open", "0", "1", "6"],
["realization-relationship", "", "block", "0", "1", "" ],
["serving-relationship", "", "open", "1", "", "" ],
["specialization-relationship", "", "block", "0", "", "" ],
["triggering-relationship", "", "block", "1", "0", "" ]
]
var diagCells = [];
var currentElement = {}; // used by the matchElement filter function
var currentRelationship = {}; // used by the matchRelationship filter function
var foundElemCount = 0;
var newElemCount = 0;
var foundRelCount = 0;
var newRelCount = 0;
// Parse a style string (= series of key-value pairs separated by ";") into an object
function parseStyle (s) {
var style = {};
var stylePair = s.split(";");
for (p in stylePair) {
var kv = stylePair[p].split("=");
if (kv[0]!="") {// the last pair is usually empty (style string ends with ";")
if (kv.length>1) {
style[kv[0]] = kv[1];
} else { // some "pairs" (e.g. edgeLabel) are just keys without value: we collect those into a "tags" property
style["tags"] = (style["tags"]===undefined ? "": style["tags"]) + "#"+kv[0];
};
};
};
return style;
};
// Expand all style strings into objets
/* function expandStyles (elem) {
for (k in elem) {
if (k=="@style") {
elem[k] = parseStyle(elem[k]);
} else if (typeof elem[k]=="object") {
expandStyles(elem[k]);
};
};
}; */
// Collect all the useful "mxCell" elements from the draw.io data
function cellsFromDrawio (elem,inarr) {
var cells = [];
for (k in elem) {
if (typeof elem[k]=="object") {
if (inarr=="mxCell" || k=="mxCell") {
if (elem[k]["@style"] !== undefined) {
var newCell = {
id: elem[k]["@id"],
value: elem[k]["@value"],
style: parseStyle(elem[k]["@style"]),
parent: elem[k]["@parent"],
source: elem[k]["@source"],
target: elem[k]["@target"]
};
if (elem[k].mxGeometry !== undefined) {
newCell.x = elem[k].mxGeometry["@x"];
newCell.y = elem[k].mxGeometry["@y"];
newCell.width = elem[k].mxGeometry["@width"];
newCell.height = elem[k].mxGeometry["@height"];
};
cells.push(newCell);
};
}
cells=cells.concat(cellsFromDrawio(elem[k],elem[k].length>0?k:undefined));
}
}
return cells
};
function matchProp (p,s) {
if (p===undefined || p===null) {
return s=="";
} else {
return p.indexOf(s)>-1;
};
};
function matchElement (eprops) {
// debug?console.log("// ",eprops,currentElement.value,currentElement.style.shape,currentElement.style.appType,currentElement.style.techType,currentElement.style.fillColor):null;
return (
matchProp(currentElement.style.tags,eprops[1]) ||
matchProp(currentElement.style.shape,eprops[1]) ||
matchProp(currentElement.style.appType,eprops[1]) ||
matchProp(currentElement.style.archiType,eprops[1]) ||
matchProp(currentElement.style.techType,eprops[1])
) && matchProp(currentElement.style.fillColor,eprops[2]);
};
function matchRelationship (rprops) {
//debug?console.log("// ",rprops,currentRelationship.value,currentRelationship.style.shape,currentRelationship.style.appType,currentRelationship.style.techType,currentRelationship.style.fillColor):null;
return (
matchProp(currentRelationship.style.startArrow, rprops[1]) &&
matchProp(currentRelationship.style.endArrow, rprops[2]) &&
matchProp(currentRelationship.style.endFill, rprops[3]) &&
matchProp(currentRelationship.style.dashed, rprops[4]) &&
matchProp(currentRelationship.style.dashPattern, rprops[5])
);
};
function createArchiElements(cells) {
// Scan all cells and try to match them to Archimate elements using the archiElemMap data
// Every time we find a matching cell, try to create a corresponding object model in Archi
debug?console.log("//","Pass 1: Elements ("+cells.length+")"):null;
for (c in cells) {
currentElement = cells[c];
debug?console.log("// ",c,":",currentElement.value,currentElement.style.shape,currentElement.style.appType,currentElement.style.techType,currentElement.style.fillColor):null;
if (currentElement.style!==undefined) {
var curElemMatch = archiElemMap.filter(matchElement);
debug?console.log(superString(curElemMatch,"// ")):null;
if (curElemMatch.length > 0) {
// We found a cell that matches at least one Archimate element - now create the corresponding model object in Archi
currentElement.type = curElemMatch[0][0];
foundElemCount++;
var archiType = curElemMatch[0][0].split("_"); // junctions can be either "junction_and" or "junction_or", so we need to extract both parts
var objElem = $("."+currentElement.value).filter("element").filter(archiType[0]); // Find out whether the element already exists in the model
if (objElem.length == 0 || forceImport) {
// Element does not exist in model yet, or forced import is required --> create it
debug?console.log("// --> creating new object model:",archiType[0]+":"+currentElement.value):null;
var newElem = model.createElement(archiType[0],currentElement.value);
if (archiType[0]=="junction") {
newElem.setJunctionType(archiType[1]);
};
if (addSourceProp) {
newElem.prop("source","Imported from '"+fileName+"', source ID = "+currentElement.id);
};
currentElement.model = newElem; // reference for use when we create relationships involving this element
newElemCount++;
} else {
// element already exist: add the reference
console.log("> Info: element "+currentElement.type+":'"+currentElement.value+"' ("+currentElement.id+") already exists in the model -- not imported");
currentElement.model = objElem[0];
};
};
};
};
};
function findCells(key,val) {
return diagCells.filter(function f(c) { return c[key] == val });
};
function createArchiRelationships(cells) {
// Scan all cells and try to match them to Archimate relationships using the archiRelMap data
// Every time we find a matching cell, try to create a corresponding object model in Archi
debug?console.log("//","Pass 2: Relationships ("+cells.length+")"):null;
for (c in cells) {
currentRelationship = cells[c];
debug?console.log("// ",c,":", currentRelationship.id):null;
if (currentRelationship.style!==undefined) {
var curRelMatch = archiRelMap.filter(matchRelationship);
debug?console.log(superString(curRelMatch,"// ")):null;
if (Object.keys(curRelMatch).length > 0) {
// We found a cell that matches at least one Archimate element - now create the corresponding model object in Archi
currentRelationship.type = curRelMatch[0][0];
foundRelCount++;
var archiType = curRelMatch[0][0].split("_"); // access relationships can be either "*_read", "*_write" or "*_readwrite", so we need to extract both parts
var curSource = findCells("id",currentRelationship.source)[0];
var curTarget = findCells("id",currentRelationship.target)[0];
if (curSource!==undefined && curTarget!==undefined) {
debug?console.log("// ",curRelMatch[0][0]+":"+currentRelationship.id,"FROM",curSource.value,"TO",curTarget.value):null;
if (currentRelationship.value=="") {
// Instead of the value, attempt to find a label attached to the relationship
var label = findCells("parent",currentRelationship.id).filter(function f(c) {return c.style.tags.indexOf("#edgeLabel")>-1});
currentRelationship.value = label===undefined?"":label[0].value;
};
var objRel = $(archiType[0]).filter("."+currentRelationship.value);
if (objRel.sourceEnds("."+curSource.value).length==0 || objRel.targetEnds("."+curTarget.value)==0 || forceImport) {
// Relationship does not exist in model yet, or forced import is required --> create it
try { // trying to create an invalid relationship will raise an exception
var newRel = model.createRelationship(archiType[0],currentRelationship.value,curSource.model,curTarget.model);
if (archiType[0]=="access-relationship") {
newElem.setAccessType(archiType[1]);
};
if (addSourceProp) {
newRel.prop("source","Imported from '"+fileName+"', source ID = "+currentRelationship.id);
};
currentRelationship.model = newRel; // reference for later use
newRelCount++;
} catch(err) {
console.log("> Error: "+currentRelationship.type+":'"+currentRelationship.value+"' ("+currentRelationship.id+") from '"+curSource.value+"' to '"+curTarget.value+"' is not allowed -- not imported");
};
} else {
// Relationship may already exist (test is not perfect...)
console.log("> Info: "+currentRelationship.type+":'"+currentRelationship.value+"' ("+currentRelationship.id+") from '"+curSource.value+"' to '"+curTarget.value+"' may already exist in the model -- not imported");
};
} else {
console.log("> Error: relationship",currentRelationship.id,"is missing a",((curSource===undefined?"SOURCE and ":"")+(curTarget===undefined?"TARGET and ":"")).slice(0,-5));
};
} else {
};
};
};
};
var filePath = window.promptOpenFile({ title: "Open draw.io file", filterExtensions: ["*.drawio.xml"], fileName: "" });
if (filePath) {
console.log("> Loading draw.io file");
var fileName = filePath.replace(/^.*(\\|\/|\:)/, '');
var FileReader = Java.type("java.io.FileReader");
var Types = Java.type("java.nio.charset.StandardCharsets");
var theDrawioFile = new FileReader(filePath,Types.UTF_8);
var theDrawio ="";
var data = theDrawioFile.read();
while(data != -1) {
var theCharacter = String.fromCharCode(data);
theDrawio+=theCharacter;
data = theDrawioFile.read();
}
theDrawioFile.close();
// Convert draw.io XML data to JSON
var objDrawio = fromXML(theDrawio);
debug?console.log("//",superString(objDrawio,"// "),"\n"):null;
console.log("> Extracting shapes from draw.io data");
diagCells = cellsFromDrawio(objDrawio);
debug?console.log("//",superString(diagCells,"// "),"\n"):null;
// Model objects are created in two passes: first elements, then relationships
// This ensures that all source and target elements exist before the relationships are created
console.log("> Matching draw.io shapes to Archimate elements and creating model objects in Archi (this may take a while...)");
createArchiElements(diagCells);
console.log("> Matching draw.io connectors to Archimate relationships and creating model objects in Archi (this may take a while...)");
createArchiRelationships(diagCells);
console.log("\n> Elements found:",foundElemCount,"\n> Elements created:",newElemCount);
console.log("> Relationships found:",foundRelCount,"\n> Relationships created:",newRelCount);
// Create a new view and populate it with all the newly created objects
var importedView = model.createArchimateView(importedViewName);
console.log("\n> Creating view with newly imported objects: ",importedView);
for (c in diagCells) {
var curCell = diagCells[c];
if (curCell.type !== undefined) {
debug?console.log("// Adding "+curCell.type+":'"+curCell.value+"' to view"):null;
if (curCell.type.indexOf("relationship")==-1) {
// this is an element
var newElem = importedView.add(curCell.model,curCell.x,curCell.y,curCell.width,curCell.height);
curCell.diag=newElem;
} else {
// this is a relationship
// assumption: source and target elements appear in XML before any relationship that reference them
var newRel = importedView.add(curCell.model,findCells("id",curCell.source)[0].diag,findCells("id",curCell.target)[0].diag);
};
};
};
} else {
console.log("> Cancelled");
}
@christhearchitect
Copy link
Author

christhearchitect commented Dec 5, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment