import { XMLParser } from 'fast-xml-parser';
import * as fs from 'fs/promises';
import type {
XmiElement,
Package,
UmlClass,
Property,
Generalization,
Enumeration,
EnumerationLiteral,
DataType,
Association,
XmiIndex,
} from '../model/types.js';
// Parser options for fast-xml-parser
const parserOptions = {
ignoreAttributes: false,
attributeNamePrefix: '@_',
textNodeName: '#text',
parseAttributeValue: false,
trimValues: true,
processEntities: true,
isArray: (name: string) => {
// These elements can appear multiple times
return [
'packagedElement',
'ownedAttribute',
'ownedLiteral',
'ownedComment',
'generalization',
'packageImport',
'memberEnd',
'ownedEnd',
].includes(name);
},
};
export class XmiParser {
private index: XmiIndex;
constructor() {
this.index = this.createEmptyIndex();
}
private createEmptyIndex(): XmiIndex {
return {
byId: new Map(),
packages: new Map(),
classes: new Map(),
enumerations: new Map(),
dataTypes: new Map(),
associations: new Map(),
byName: new Map(),
classesByPackage: new Map(),
enumsByPackage: new Map(),
childrenByParent: new Map(),
subclasses: new Map(),
rootModelId: '',
};
}
async parse(filePath: string): Promise<XmiIndex> {
// Read file
const xmlContent = await fs.readFile(filePath, 'utf-8');
// Parse XML
const parser = new XMLParser(parserOptions);
const parsed = parser.parse(xmlContent);
// Get root XMI element
const xmi = parsed['xmi:XMI'];
if (!xmi) {
throw new Error('Invalid XMI file: missing xmi:XMI root element');
}
// Get UML Model
const umlModel = xmi['uml:Model'];
if (!umlModel) {
throw new Error('Invalid XMI file: missing uml:Model element');
}
// Process the model
this.processModel(umlModel);
// Process SysML stereotypes
this.processSysmlStereotypes(xmi);
// Build package paths
this.buildPackagePaths();
// Resolve type names
this.resolveTypeNames();
// Build subclass index
this.buildSubclassIndex();
return this.index;
}
private processModel(model: any): void {
const xmiId = model['@_xmi:id'];
const name = model['@_name'] || 'Model';
const pkg: Package = {
xmiId,
xmiType: 'uml:Model',
name,
childPackageIds: [],
childElementIds: [],
documentation: this.extractDocumentation(model),
};
this.index.rootModelId = xmiId;
this.index.packages.set(xmiId, pkg);
this.index.byId.set(xmiId, pkg);
this.addToNameIndex(name, xmiId);
// Process packaged elements
this.processPackagedElements(model.packagedElement, xmiId, pkg);
}
private processPackagedElements(
elements: any[] | undefined,
parentId: string,
parentPkg: Package
): void {
if (!elements) return;
for (const element of elements) {
const xmiType = element['@_xmi:type'];
const xmiId = element['@_xmi:id'];
if (!xmiId) continue;
// Track parent-child relationship
this.addToIndex(this.index.childrenByParent, parentId, xmiId);
switch (xmiType) {
case 'uml:Package':
case 'uml:Profile':
this.processPackage(element, parentId, parentPkg);
break;
case 'uml:Class':
this.processClass(element, parentId, parentPkg);
break;
case 'uml:Enumeration':
this.processEnumeration(element, parentId, parentPkg);
break;
case 'uml:DataType':
case 'uml:PrimitiveType':
this.processDataType(element, parentId, parentPkg);
break;
case 'uml:Association':
this.processAssociation(element, parentId, parentPkg);
break;
// Skip other types for now (diagrams, etc.)
}
}
}
private processPackage(element: any, parentId: string, parentPkg: Package): void {
const xmiId = element['@_xmi:id'];
const name = element['@_name'] || '';
const pkg: Package = {
xmiId,
xmiType: element['@_xmi:type'] as 'uml:Package' | 'uml:Profile',
name,
parentId,
childPackageIds: [],
childElementIds: [],
documentation: this.extractDocumentation(element),
};
this.index.packages.set(xmiId, pkg);
this.index.byId.set(xmiId, pkg);
this.addToNameIndex(name, xmiId);
parentPkg.childPackageIds.push(xmiId);
// Process nested elements
this.processPackagedElements(element.packagedElement, xmiId, pkg);
}
private processClass(element: any, parentId: string, parentPkg: Package): void {
const xmiId = element['@_xmi:id'];
const name = element['@_name'] || '';
const umlClass: UmlClass = {
xmiId,
xmiType: 'uml:Class',
name,
parentId,
packageId: parentId,
isAbstract: element['@_isAbstract'] === 'true',
properties: this.extractProperties(element),
generalizations: this.extractGeneralizations(element),
stereotypes: [],
documentation: this.extractDocumentation(element),
};
this.index.classes.set(xmiId, umlClass);
this.index.byId.set(xmiId, umlClass);
this.addToNameIndex(name, xmiId);
this.addToIndex(this.index.classesByPackage, parentId, xmiId);
parentPkg.childElementIds.push(xmiId);
}
private processEnumeration(element: any, parentId: string, parentPkg: Package): void {
const xmiId = element['@_xmi:id'];
const name = element['@_name'] || '';
const enumeration: Enumeration = {
xmiId,
xmiType: 'uml:Enumeration',
name,
parentId,
packageId: parentId,
isAbstract: element['@_isAbstract'] === 'true',
literals: this.extractLiterals(element),
documentation: this.extractDocumentation(element),
};
this.index.enumerations.set(xmiId, enumeration);
this.index.byId.set(xmiId, enumeration);
this.addToNameIndex(name, xmiId);
this.addToIndex(this.index.enumsByPackage, parentId, xmiId);
parentPkg.childElementIds.push(xmiId);
}
private processDataType(element: any, parentId: string, parentPkg: Package): void {
const xmiId = element['@_xmi:id'];
const name = element['@_name'] || '';
const dataType: DataType = {
xmiId,
xmiType: element['@_xmi:type'] as 'uml:DataType' | 'uml:PrimitiveType',
name,
parentId,
packageId: parentId,
isAbstract: element['@_isAbstract'] === 'true',
documentation: this.extractDocumentation(element),
};
this.index.dataTypes.set(xmiId, dataType);
this.index.byId.set(xmiId, dataType);
this.addToNameIndex(name, xmiId);
parentPkg.childElementIds.push(xmiId);
}
private processAssociation(element: any, parentId: string, parentPkg: Package): void {
const xmiId = element['@_xmi:id'];
const name = element['@_name'] || '';
const association: Association = {
xmiId,
xmiType: 'uml:Association',
name: name || undefined,
parentId,
memberEndIds: this.extractMemberEndIds(element),
ownedEndIds: this.extractOwnedEndIds(element),
documentation: this.extractDocumentation(element),
};
this.index.associations.set(xmiId, association);
this.index.byId.set(xmiId, association);
if (name) this.addToNameIndex(name, xmiId);
parentPkg.childElementIds.push(xmiId);
}
private extractProperties(element: any): Property[] {
const properties: Property[] = [];
const attrs = element.ownedAttribute;
if (!attrs) return properties;
for (const attr of attrs) {
const prop: Property = {
xmiId: attr['@_xmi:id'] || '',
name: attr['@_name'] || '',
typeId: attr['@_type'],
visibility: (attr['@_visibility'] as Property['visibility']) || 'public',
aggregation: (attr['@_aggregation'] as Property['aggregation']) || 'none',
lowerBound: this.extractLowerBound(attr),
upperBound: this.extractUpperBound(attr),
documentation: this.extractDocumentation(attr),
defaultValue: this.extractDefaultValue(attr),
};
properties.push(prop);
}
return properties;
}
private extractLowerBound(attr: any): number {
const val = attr.lowerValue;
if (!val) return 0;
const value = val['@_value'];
return parseInt(value, 10) || 0;
}
private extractUpperBound(attr: any): number | string {
const val = attr.upperValue;
if (!val) return 1;
const value = val['@_value'];
if (value === '*') return '*';
return parseInt(value, 10) || 1;
}
private extractDefaultValue(attr: any): string | undefined {
const defVal = attr.defaultValue;
if (!defVal) return undefined;
return defVal['@_value'] || defVal['@_body'];
}
private extractGeneralizations(element: any): Generalization[] {
const generalizations: Generalization[] = [];
const gens = element.generalization;
if (!gens) return generalizations;
for (const gen of gens) {
const g: Generalization = {
xmiId: gen['@_xmi:id'] || '',
generalId: gen['@_general'] || '',
};
generalizations.push(g);
}
return generalizations;
}
private extractLiterals(element: any): EnumerationLiteral[] {
const literals: EnumerationLiteral[] = [];
const lits = element.ownedLiteral;
if (!lits) return literals;
for (const lit of lits) {
const l: EnumerationLiteral = {
xmiId: lit['@_xmi:id'] || '',
name: lit['@_name'] || '',
documentation: this.extractDocumentation(lit),
};
literals.push(l);
}
return literals;
}
private extractMemberEndIds(element: any): string[] {
const ids: string[] = [];
const ends = element.memberEnd;
if (!ends) return ids;
for (const end of ends) {
const idref = end['@_xmi:idref'];
if (idref) ids.push(idref);
}
return ids;
}
private extractOwnedEndIds(element: any): string[] {
const ids: string[] = [];
const ends = element.ownedEnd;
if (!ends) return ids;
for (const end of ends) {
const id = end['@_xmi:id'];
if (id) ids.push(id);
}
return ids;
}
private extractDocumentation(element: any): string | undefined {
const comments = element.ownedComment;
if (!comments) return undefined;
const bodies: string[] = [];
for (const comment of comments) {
const body = comment['@_body'];
if (body) {
// Clean up the body text
bodies.push(body.replace(/ /g, '\n').replace(/ /g, ''));
}
// Check for nested comments
const nestedDoc = this.extractDocumentation(comment);
if (nestedDoc) bodies.push(nestedDoc);
}
return bodies.length > 0 ? bodies.join('\n\n') : undefined;
}
private processSysmlStereotypes(xmi: any): void {
// Look for sysml:Block elements
const blocks = xmi['sysml:Block'];
if (blocks) {
const blockArray = Array.isArray(blocks) ? blocks : [blocks];
for (const block of blockArray) {
const baseClassId = block['@_base_Class'];
if (baseClassId) {
const cls = this.index.classes.get(baseClassId);
if (cls) {
cls.stereotypes.push('Block');
}
}
}
}
// Look for sysml:ValueType
const valueTypes = xmi['sysml:ValueType'];
if (valueTypes) {
const vtArray = Array.isArray(valueTypes) ? valueTypes : [valueTypes];
for (const vt of vtArray) {
const baseTypeId = vt['@_base_DataType'];
if (baseTypeId) {
const dt = this.index.dataTypes.get(baseTypeId);
if (dt) {
// Mark as value type in documentation
}
}
}
}
}
private buildPackagePaths(): void {
const buildPath = (pkgId: string, pathParts: string[]): void => {
const pkg = this.index.packages.get(pkgId);
if (!pkg) return;
const currentPath = [...pathParts, pkg.name || ''].filter(Boolean);
pkg.path = currentPath.join('/');
for (const childId of pkg.childPackageIds) {
buildPath(childId, currentPath);
}
};
buildPath(this.index.rootModelId, []);
}
private resolveTypeNames(): void {
for (const cls of this.index.classes.values()) {
for (const prop of cls.properties) {
if (prop.typeId) {
const typeElement = this.index.byId.get(prop.typeId);
if (typeElement && typeElement.name) {
prop.typeName = typeElement.name;
}
}
}
for (const gen of cls.generalizations) {
const generalElement = this.index.byId.get(gen.generalId);
if (generalElement && generalElement.name) {
gen.generalName = generalElement.name;
}
}
}
}
private buildSubclassIndex(): void {
for (const cls of this.index.classes.values()) {
for (const gen of cls.generalizations) {
this.addToIndex(this.index.subclasses, gen.generalId, cls.xmiId);
}
}
}
private addToNameIndex(name: string, xmiId: string): void {
if (!name) return;
this.addToIndex(this.index.byName, name, xmiId);
}
private addToIndex(map: Map<string, string[]>, key: string, value: string): void {
const existing = map.get(key);
if (existing) {
existing.push(value);
} else {
map.set(key, [value]);
}
}
}