Archi, jArchi (CSV Import/Export) and Specialisations (oh my)

  1. Specializations
  2. Import/Export jArchi Scripts
    1. Export to CSV
    2. Import from CSV

Specializations

Archi 4.9 introduced support for the concept of “ArchiMate Specializations“, this is a feature that the ArchiMate “language” has long supported, but had been manually applied in Archi before the new release. You can see my usage of Specialization in my post on the Technology Layer.

Here, there are three Specializations of the Artifact Element:

  • Software
  • Operating System
  • Database

From an Archi point of view, this was managed via naming the Element with the “<<>>” notation:

Specialization in Element name

However, with the new support for Specializations in Archi, we can now achieve this with a Specialization of “Software”:

Specialization held in specific field

This makes our naming clearer, and allows us to filter our model based on the Specialization when looking for those types:

You can tell from the list of Specializations in my model, that I’m a fan…

Archi 4.7 introduced the concept of “Labels” to the tool, this allows us to “override” the name of an element where appropriate. By applying a label expression to the Object on our View, we can now highlight the Specialization in use.

Label Expression = ${name}${if:${specialization}: «${specialization}»}

We can take this a step further, by using the Archi support of Specialization images. By applying an image to each of our Specializations, they will show in the top right of the Element when viewed in the diagram, this can be seen in the example below, showing the “Folder”, “CSV”, “Script” and “Software” Specializations moving from the “name” of the element, to Specialization with an image provided (icons from IconMonstr)

Import/Export jArchi Scripts

So far so good… but I also wrote a number of scripts using jArchi to import and export views from my model to/from CSV format. These scripts have now been updated to support the Specialization concept.

Export to CSV

The “Export to CSV” script hosted on Github Gists has been updated to also output the Specialization of an Element to a column in the file.

/*
* Export To CSV
*
* Requires jArchi – https://www.archimatetool.com/blog/2018/07/02/jarchi/
* Requires PapaParse – https://www.papaparse.com/
* Works with Import from CSV script – https://gist.github.com/smileham/1e57a5946235e780dee5a824f664aa3d
*
* Version 1: Export to CSV
* Version 1.1: Avoid duplicate concepts exported from diagram
* Version 1.2: Fix missing properties
* Version 2: Updated to export Relationships to additional CSV
* Version 2.1: Added error check for View.
* Version 2.2: Added support for Specialization
* Version 2.3: Added UTF-8 BOM to (hopefully) fix Excel export
* Version 2.4: Added support for relationship properties export (by @isalew)
*
* (c) 2018 Steven Mileham
*
*/
const debug = false;
// Show output in the console
console.show();
console.clear();
console.log("> Starting CSV Export");
const Papa = require(__DIR__ + "lib/papaparse.min.js");
let propertiesList = [];
let relationPropertiesList = [];
let conceptHashMap = [];
// Set up some conceptHeaders
const conceptHeaders = [
"Name",
"Documentation",
"UID",
"Type",
"Specialization"
];
const relationshipHeaders = [
"Relationship ID",
"From Name",
"From Type",
"Relationship Type",
"Specialization",
"To Name",
"To Type",
"Relationship Name",
"Relationship Documentation"
];
const typeMappings = {
"access-relationship":"Accesses",
"composition-relationship":"Comprises",
"flow-relationship":"Flows to",
"realization-relationship":"Realises",
"assignment-relationship":"Assigned to",
"serving-relationship":"Serves/Used By",
"association-relationship":"Associated to",
"aggregation-relationship":"Aggregates"
}
let current_row = 1;
let theData = new Array();
let theRelationshipData = new Array();
const theView = $(selection).filter("archimate-diagram-model").first();
debug? console.log(theView):true;
if (!theView) {
console.log("> Please Select a View");
}
else {
// Loop through all elements and set cells to elememt info
$(theView).find().not("relationship").each(function(e) {
const theConcept = e.concept;
try {
if (e.name!="") {
if (!conceptHashMap[theConcept.id]) {
conceptHashMap[theConcept.id]=true;
const theProperties = theConcept.prop();
for (var i=0; i<theProperties.length; i++){
let found = false;
for (var j=0; j<propertiesList.length; j++) {
if (propertiesList[j]==theProperties[i]) {
found=true;
}
}
if (!found) {
propertiesList.push(theProperties[i]);
conceptHeaders.push(theProperties[i]);
}
}
let theObject = new Object;
theObject["Name"]=theConcept.name;
theObject["Documentation"]=theConcept.documentation;
theObject["UID"]=theConcept.id;
theObject["Type"]=theConcept.type;
theObject["Specialization"]=theConcept.specialization;
for (var i=0; i<propertiesList.length; i++){
if (theConcept.prop(propertiesList[i])) {
theObject[propertiesList[i]]=""+theConcept.prop(propertiesList[i]);
}
}
debug? console.log("> theObject"):true;
debug? console.log(theObject):true;
theData.push(theObject);
// Get Relationships
$(e).outRels().each(function (r) {
let theRelationshipRow = new Object;
theRelationshipRow["Relationship ID"]=r.id;
theRelationshipRow["From Name"]=r.source.name;
theRelationshipRow["From Type"]=r.source.type;
theRelationshipRow["Relationship Type"]=!typeMappings[r.type]?r.type:typeMappings[r.type];
theRelationshipRow["Specialization"]=r.specialization;
theRelationshipRow["To Name"]=r.target.name;
theRelationshipRow["To Type"]=r.target.type;
theRelationshipRow["Relationship Name"]=r.name;
theRelationshipRow["Relationship Documentation"]=r.documentation;
const theRelationshipProperties = r.prop();
for (var i=0; i<theRelationshipProperties.length; i++){
let found = false;
for (var j=0; j<relationPropertiesList.length; j++) {
if (relationPropertiesList[j]==theRelationshipProperties[i]) {
found=true;
}
}
if (!found) {
relationPropertiesList.push(theRelationshipProperties[i]);
relationshipHeaders.push(theRelationshipProperties[i]);
}
}
for (var i=0; i<relationPropertiesList.length; i++){
if (r.prop(relationPropertiesList[i])) {
theRelationshipRow[relationPropertiesList[i]]=""+r.prop(relationPropertiesList[i]);
}
}
debug? console.log("> theRelationshipRow"):true;
debug? console.log(theRelationshipRow):true;
theRelationshipData.push(theRelationshipRow);
});
current_row++;
}
else {
console.log("Duplicate Concept: ",theConcept.name);
}
}
}
catch (error) {
console.log("> Ignoring: "+e);
}
});
// Open a dialog to let the user choose where to save the generated file
const defaultFileName = model.name ? model.name + "-" + theView.name + ".csv" : "Exported Model.csv"; // Default file name
const exportFile = window.promptSaveFile({ title: "Export to CSV", filterExtensions: [ "*.csv" ], fileName: defaultFileName } );
debug? console.log("> conceptHeaders"+conceptHeaders):true;
debug? console.log("> TheData"+theData.length):true;
debug? console.log(theData):true;
debug? console.log(theRelationshipData):true;
const theCSV = Papa.unparse({fields:conceptHeaders, data:theData});
const theRelationshipsCSV = Papa.unparse({fields:relationshipHeaders, data:theRelationshipData});
if(exportFile != null) {
debug? console.log("> TheCSV"):true;
debug? console.log(theCSV):true;
$.fs.writeFile(exportFile, "\ufeff"+theCSV);
$.fs.writeFile(exportFile.substring(0,exportFile.length-4) +"-relationship.csv", "\ufeff"+theRelationshipsCSV);
console.log("> Export done");
}
else {
console.log("> Export cancelled");
}
}

Import from CSV

The “Import from CSV” script hosted on Github Gists has been updated read the Specialization column, it will then match to an existing Specialization for the given Element type (if found), and if not, create that Specialization in the model.

The script has also been updated to add each Element imported to a new View in the root of the model. Each Element will be added to a Diagram Group to highlight whether it was matched via UID, name, or whether it was created as a new Element.

/*
* Import from CSV
*
* Requires jArchi – https://www.archimatetool.com/blog/2018/07/02/jarchi/
* Requires PapaParse – https://www.papaparse.com/
* Works with Export to CSV Script – https://gist.github.com/smileham/15c445b17a92bd6f5dc1508e573bcd8a
*
* Version 1: Import from CSV
* Version 1.1: Force character encoding to use UTF-8
* Version 2: Support for Specialization and creates "CSVImport-timestamp" view
* Version 2.1: Fix for "empty" rows
* Version 2.2: Fix for BOM
* Version 2.3: Enhanced error detection
*
* (c) 2025 Steven Mileham
*
*/
function parseCSV(filePath) {
const Papa = require(__DIR__ + "lib/papaparse.min.js");
console.log("> Loaded Papa Parse");
let FileReader = Java.type("java.io.FileReader");
const Types = Java.type("java.nio.charset.StandardCharsets");
let theCSVFile = new FileReader(filePath,Types.UTF_8);
let theCSV ="";
let data = theCSVFile.read();
console.log("> Please Wait…");
while(data != -1) {
if (data!=65279) {
let theCharacter = String.fromCharCode(data);
theCSV+=theCharacter;
}
data = theCSVFile.read();
}
theCSVFile.close();
console.log("> File Loaded");
return Papa.parse(theCSV);
}
var debug = false;
console.show();
console.clear();
console.log("> Import CSV");
const filePath = window.promptOpenFile({ title: "Open CSV", filterExtensions: ["*.CSV"], fileName: "default.csv" });
if (filePath) {
theDataFile = parseCSV(filePath);
theData = theDataFile.data;
if (theDataFile.errors.length > 0) {
let errorMsg = "Errors found in CSV file:\n";
theDataFile.errors.forEach(function(error) {
errorMsg += `- Row ${error.row}: ${error.message}\n`;
});
console.error(errorMsg);
}
else {
theDataHeaders = theData[0];
const commonProperties = ["UID","Name", "Documentation","Type","Specialization"];
let missingColumns = commonProperties.filter(col => !theDataHeaders.includes(col));
if (missingColumns.length > 0) {
let errorMsg = `The CSV is missing the following required columns: ${missingColumns.join(", ")}`;
console.error(errorMsg);
}
else {
let newGroupStart = 48;
let nameGroupStart = 180;
let uidGroupStart = 312;
const maxPerRow = 10;
let newGroupCount=0;
let nameGroupCount=0;
let uidGroupCount=0;
let newGroupRow=1;
let nameGroupRow=1;
let uidGroupRow=1;
const theDate = new Date();
let theView = model.createArchimateView("CSVImport-"+theDate.toString());
let templateObject = theView.createObject("note", 0, 0, -1, -1);
let templateHeight = templateObject.bounds.height;
let templateWidth = templateObject.bounds.width
templateObject.delete();
let titleNote = theView.createObject("note", 12, 12, maxPerRow*templateWidth, 24);
titleNote.text = "Import from:"+filePath+" on "+theDate.toString();
let newGroup = theView.createObject("diagram-model-group", 12, newGroupStart, maxPerRow*templateWidth, templateWidth);
newGroup.name = "New Elements";
let nameGroup = theView.createObject("diagram-model-group", 12, nameGroupStart, maxPerRow*templateWidth, templateWidth);
nameGroup.name = "Matched by Name";
let uidGroup = theView.createObject("diagram-model-group", 12, uidGroupStart, maxPerRow*templateWidth, templateWidth);
uidGroup.name = "Matched by UID";
const commonProperties = ["UID","Name", "Documentation","Type","Specialization"];
for (let i=1; i<theData.length; i++) {
let matchType = 3;
let theConcept = null;
let theObject = [];
for (let j=0; j<theDataHeaders.length; j++) {
theObject[theDataHeaders[j]]=theData[i][j];
}
debug? console.log(theObject):true;
if (theObject.Name!=null && theObject.Name.trim()!="" && theObject.Name.trim()!="null") {
theConcept = $("#"+theObject.UID).first();
if (!theConcept) {
debug? console.log("> Missing UID, checking Name"):true;
theConcept = $("."+theObject.Name).first();
matchType=2;
if (!theConcept || theConcept.length>1) {
debug? console.log("> Creating Concept"):true;
theConcept = model.createElement(theObject.Type,theObject.Name);
matchType=1;
}
}
debug? console.log(theConcept):true;
theConcept.name=theObject.Name;
theConcept.documentation=theObject.Documentation;
theConcept.type=theObject.Type;
if (theObject.Specialization!="")
{
theSpecializations = model.specializations;
for (let j=0; j<theSpecializations.length; j++) {
debug? console.log("> Checking Spec " + theSpecializations[j]):true;
if (theSpecializations[j].name==theObject.Specialization && theSpecializations[j].type == theObject.Type)
{
debug? console.log("> Setting Spec " + theConcept.specialization):true;
theConcept.specialization=theObject.Specialization;
}
}
if (theConcept.specialization==null)
{
debug? console.log("> Creating Spec " + theObject.Specialization):true;
model.createSpecialization(theObject.Specialization, theObject.Type);
theConcept.specialization=theObject.Specialization;
}
}
for (let j=0; j<theDataHeaders.length; j++) {
switch (theDataHeaders[j]) {
case "UID":
case "Name":
case "Documentation":
case "Type":
case "Specialization":
break;
default:
if (theObject[theDataHeaders[j]]) {
theConcept.prop(theDataHeaders[j],theObject[theDataHeaders[j]]);
}
else {
theConcept.removeProp(theDataHeaders[j]);
}
}
}
newGroup.bounds = {x:newGroup.bounds.x, y:newGroup.bounds.y, width:maxPerRow*templateWidth,height:(templateHeight+12)*newGroupRow+24};
nameGroup.bounds = {x:nameGroup.bounds.x, y:newGroup.bounds.y+newGroup.bounds.height+12, width:maxPerRow*templateWidth,height:(templateHeight+12)*nameGroupRow+24};
uidGroup.bounds = {x:uidGroup.bounds.x, y:nameGroup.bounds.y+nameGroup.bounds.height+12, width:maxPerRow*templateWidth,height:(templateHeight+12)*uidGroupRow+24};
switch (matchType) {
case 3:
uidGroupCount++;
if (uidGroupCount>=maxPerRow)
{
uidGroupRow++;
uidGroupCount=1;
}
theGroupStart = uidGroup.bounds.y;
theGroupCount = uidGroupCount;
theGroupRow = uidGroupRow;
break;
case 2:
debug? console.log("> Name Group Width " +nameGroup.bounds.width):true;
nameGroupCount++;
if (nameGroupCount>=maxPerRow)
{
nameGroupRow++;
nameGroupCount=1;
}
theGroupStart = nameGroup.bounds.y;
theGroupCount = nameGroupCount;
theGroupRow = nameGroupRow;
debug? console.log("> NEW Name Group Width " +nameGroup.bounds.width):true;
break;
case 1:
newGroupCount++;
if (newGroupCount>=maxPerRow)
{
newGroupRow++;
newGroupCount=1;
}
theGroupStart = newGroup.bounds.y;
theGroupCount = newGroupCount;
theGroupRow = newGroupRow;
break;
}
let theViewObject = theView.add(theConcept, (theGroupCount*templateWidth)-(templateWidth-12)+(theGroupCount*12), theGroupStart+12+((theGroupRow-1)*templateHeight)+(theGroupRow*12), -1, -1, true);
}
}
}
}
console.log("> Parsing Complete")
}
else {
console.log("> Cancelled");
}

2 comments

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.