import type {
XmiElement,
Package,
UmlClass,
Property,
Enumeration,
DataType,
XmiIndex,
SearchResult,
ModelStatistics,
} from './types.js';
export class XmiModel {
private index: XmiIndex;
private documentationIndex: Map<string, Set<string>>; // word -> Set<xmiId>
constructor(index: XmiIndex) {
this.index = index;
this.documentationIndex = new Map();
this.buildDocumentationIndex();
}
// ============ Statistics ============
getStatistics(): ModelStatistics {
let propertyCount = 0;
for (const cls of this.index.classes.values()) {
propertyCount += cls.properties.length;
}
return {
packageCount: this.index.packages.size,
classCount: this.index.classes.size,
enumerationCount: this.index.enumerations.size,
dataTypeCount: this.index.dataTypes.size,
propertyCount,
associationCount: this.index.associations.size,
};
}
// ============ Element Access ============
getElementById(id: string): XmiElement | undefined {
return this.index.byId.get(id);
}
getElementsByName(name: string): XmiElement[] {
const ids = this.index.byName.get(name) || [];
return ids.map(id => this.index.byId.get(id)).filter((e): e is XmiElement => e !== undefined);
}
// ============ Package Queries ============
getPackages(): Package[] {
return Array.from(this.index.packages.values());
}
getPackage(identifier: string): Package | undefined {
// Try by ID first
let pkg = this.index.packages.get(identifier);
if (pkg) return pkg;
// Try by name
const ids = this.index.byName.get(identifier) || [];
for (const id of ids) {
pkg = this.index.packages.get(id);
if (pkg) return pkg;
}
return undefined;
}
getPackageHierarchy(packageId: string): Package[] {
const hierarchy: Package[] = [];
let currentId: string | undefined = packageId;
while (currentId) {
const pkg = this.index.packages.get(currentId);
if (pkg) {
hierarchy.unshift(pkg);
currentId = pkg.parentId;
} else {
break;
}
}
return hierarchy;
}
getChildPackages(packageId: string): Package[] {
const pkg = this.index.packages.get(packageId);
if (!pkg) return [];
return pkg.childPackageIds
.map(id => this.index.packages.get(id))
.filter((p): p is Package => p !== undefined);
}
getRootPackage(): Package | undefined {
return this.index.packages.get(this.index.rootModelId);
}
listPackages(options: {
parentPackage?: string;
recursive?: boolean;
namePattern?: string;
} = {}): Package[] {
let packages: Package[];
if (options.parentPackage) {
const parent = this.getPackage(options.parentPackage);
if (!parent) return [];
if (options.recursive) {
packages = this.getPackagesRecursive(parent.xmiId);
} else {
packages = this.getChildPackages(parent.xmiId);
}
} else {
packages = this.getPackages();
}
if (options.namePattern) {
const regex = new RegExp(options.namePattern, 'i');
packages = packages.filter(p => p.name && regex.test(p.name));
}
return packages;
}
private getPackagesRecursive(packageId: string): Package[] {
const result: Package[] = [];
const children = this.getChildPackages(packageId);
for (const child of children) {
result.push(child);
result.push(...this.getPackagesRecursive(child.xmiId));
}
return result;
}
// ============ Class Queries ============
getClasses(): UmlClass[] {
return Array.from(this.index.classes.values());
}
getClass(identifier: string): UmlClass | undefined {
// Try by ID first
let cls = this.index.classes.get(identifier);
if (cls) return cls;
// Try by name
const ids = this.index.byName.get(identifier) || [];
for (const id of ids) {
cls = this.index.classes.get(id);
if (cls) return cls;
}
return undefined;
}
getClassesInPackage(packageId: string, recursive: boolean = false): UmlClass[] {
const classIds = this.index.classesByPackage.get(packageId) || [];
const classes = classIds
.map(id => this.index.classes.get(id))
.filter((c): c is UmlClass => c !== undefined);
if (recursive) {
const childPackageIds = this.index.packages.get(packageId)?.childPackageIds || [];
for (const childId of childPackageIds) {
classes.push(...this.getClassesInPackage(childId, true));
}
}
return classes;
}
findClasses(options: {
name?: string;
namePattern?: string;
package?: string;
recursive?: boolean;
includeAbstract?: boolean;
} = {}): UmlClass[] {
let classes: UmlClass[];
if (options.package) {
const pkg = this.getPackage(options.package);
if (!pkg) return [];
classes = this.getClassesInPackage(pkg.xmiId, options.recursive !== false);
} else {
classes = this.getClasses();
}
if (options.name) {
classes = classes.filter(c => c.name === options.name);
}
if (options.namePattern) {
const regex = new RegExp(options.namePattern, 'i');
classes = classes.filter(c => c.name && regex.test(c.name));
}
if (options.includeAbstract === false) {
classes = classes.filter(c => !c.isAbstract);
}
return classes;
}
getInheritanceChain(classId: string, direction: 'ancestors' | 'descendants' | 'both' = 'both'): {
class: UmlClass | undefined;
ancestors: UmlClass[];
descendants: UmlClass[];
} {
const cls = this.index.classes.get(classId) || this.getClass(classId);
const result = {
class: cls,
ancestors: [] as UmlClass[],
descendants: [] as UmlClass[],
};
if (!cls) return result;
if (direction === 'ancestors' || direction === 'both') {
result.ancestors = this.getAncestors(cls.xmiId);
}
if (direction === 'descendants' || direction === 'both') {
result.descendants = this.getDescendants(cls.xmiId);
}
return result;
}
private getAncestors(classId: string, visited: Set<string> = new Set()): UmlClass[] {
if (visited.has(classId)) return [];
visited.add(classId);
const cls = this.index.classes.get(classId);
if (!cls) return [];
const ancestors: UmlClass[] = [];
for (const gen of cls.generalizations) {
const parent = this.index.classes.get(gen.generalId);
if (parent) {
ancestors.push(parent);
ancestors.push(...this.getAncestors(parent.xmiId, visited));
}
}
return ancestors;
}
private getDescendants(classId: string, visited: Set<string> = new Set()): UmlClass[] {
if (visited.has(classId)) return [];
visited.add(classId);
const subclassIds = this.index.subclasses.get(classId) || [];
const descendants: UmlClass[] = [];
for (const subId of subclassIds) {
const sub = this.index.classes.get(subId);
if (sub) {
descendants.push(sub);
descendants.push(...this.getDescendants(subId, visited));
}
}
return descendants;
}
getClassWithInheritedProperties(classId: string): {
class: UmlClass | undefined;
allProperties: Property[];
} {
const cls = this.index.classes.get(classId) || this.getClass(classId);
if (!cls) return { class: undefined, allProperties: [] };
const allProperties: Property[] = [...cls.properties];
const ancestors = this.getAncestors(cls.xmiId);
for (const ancestor of ancestors) {
for (const prop of ancestor.properties) {
// Add if not overridden
if (!allProperties.some(p => p.name === prop.name)) {
allProperties.push({ ...prop, typeName: `${prop.typeName} (inherited from ${ancestor.name})` });
}
}
}
return { class: cls, allProperties };
}
// ============ Enumeration Queries ============
getEnumerations(): Enumeration[] {
return Array.from(this.index.enumerations.values());
}
getEnumeration(identifier: string): Enumeration | undefined {
// Try by ID first
let enumeration = this.index.enumerations.get(identifier);
if (enumeration) return enumeration;
// Try by name
const ids = this.index.byName.get(identifier) || [];
for (const id of ids) {
enumeration = this.index.enumerations.get(id);
if (enumeration) return enumeration;
}
return undefined;
}
listEnumerations(options: {
package?: string;
namePattern?: string;
} = {}): Enumeration[] {
let enums: Enumeration[];
if (options.package) {
const pkg = this.getPackage(options.package);
if (!pkg) return [];
const enumIds = this.index.enumsByPackage.get(pkg.xmiId) || [];
enums = enumIds
.map(id => this.index.enumerations.get(id))
.filter((e): e is Enumeration => e !== undefined);
} else {
enums = this.getEnumerations();
}
if (options.namePattern) {
const regex = new RegExp(options.namePattern, 'i');
enums = enums.filter(e => e.name && regex.test(e.name));
}
return enums;
}
// ============ DataType Queries ============
getDataType(identifier: string): DataType | undefined {
// Try by ID first
let dt = this.index.dataTypes.get(identifier);
if (dt) return dt;
// Try by name
const ids = this.index.byName.get(identifier) || [];
for (const id of ids) {
dt = this.index.dataTypes.get(id);
if (dt) return dt;
}
return undefined;
}
// ============ Search ============
searchByName(query: string, options: {
exactMatch?: boolean;
elementTypes?: string[];
} = {}): SearchResult[] {
const results: SearchResult[] = [];
if (options.exactMatch) {
const elements = this.getElementsByName(query);
for (const el of elements) {
if (this.matchesElementType(el, options.elementTypes)) {
results.push(this.elementToSearchResult(el));
}
}
} else {
const queryLower = query.toLowerCase();
for (const [name, ids] of this.index.byName.entries()) {
if (name.toLowerCase().includes(queryLower)) {
for (const id of ids) {
const el = this.index.byId.get(id);
if (el && this.matchesElementType(el, options.elementTypes)) {
results.push(this.elementToSearchResult(el));
}
}
}
}
}
return results;
}
searchDocumentation(query: string, options: {
elementTypes?: string[];
limit?: number;
} = {}): SearchResult[] {
const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 2);
if (words.length === 0) return [];
// Find elements that match all words
let matchingIds: Set<string> | undefined;
for (const word of words) {
const idsForWord = this.documentationIndex.get(word);
if (!idsForWord) {
return []; // No matches for this word
}
if (!matchingIds) {
matchingIds = new Set(idsForWord);
} else {
// Intersection
matchingIds = new Set([...matchingIds].filter(id => idsForWord.has(id)));
}
}
if (!matchingIds || matchingIds.size === 0) return [];
const results: SearchResult[] = [];
const limit = options.limit || 50;
for (const id of matchingIds) {
if (results.length >= limit) break;
const el = this.index.byId.get(id);
if (el && this.matchesElementType(el, options.elementTypes)) {
const result = this.elementToSearchResult(el);
// Add snippet from documentation
if (el.documentation) {
const snippet = this.extractSnippet(el.documentation, words[0]);
result.snippet = snippet;
}
results.push(result);
}
}
return results;
}
private buildDocumentationIndex(): void {
for (const el of this.index.byId.values()) {
if (el.documentation) {
const words = el.documentation.toLowerCase().split(/\W+/).filter(w => w.length > 2);
for (const word of words) {
let ids = this.documentationIndex.get(word);
if (!ids) {
ids = new Set();
this.documentationIndex.set(word, ids);
}
ids.add(el.xmiId);
}
}
}
// Also index enumeration literals
for (const enumeration of this.index.enumerations.values()) {
for (const lit of enumeration.literals) {
if (lit.documentation) {
const words = lit.documentation.toLowerCase().split(/\W+/).filter(w => w.length > 2);
for (const word of words) {
let ids = this.documentationIndex.get(word);
if (!ids) {
ids = new Set();
this.documentationIndex.set(word, ids);
}
ids.add(enumeration.xmiId);
}
}
}
}
}
private matchesElementType(el: XmiElement, types?: string[]): boolean {
if (!types || types.length === 0) return true;
const elType = this.getElementTypeLabel(el);
return types.some(t => t.toLowerCase() === elType.toLowerCase());
}
private getElementTypeLabel(el: XmiElement): string {
if (this.index.packages.has(el.xmiId)) return 'package';
if (this.index.classes.has(el.xmiId)) return 'class';
if (this.index.enumerations.has(el.xmiId)) return 'enumeration';
if (this.index.dataTypes.has(el.xmiId)) return 'datatype';
if (this.index.associations.has(el.xmiId)) return 'association';
return 'element';
}
private elementToSearchResult(el: XmiElement): SearchResult {
const pkg = el.parentId ? this.index.packages.get(el.parentId) : undefined;
return {
elementType: this.getElementTypeLabel(el),
xmiId: el.xmiId,
name: el.name || '(unnamed)',
path: pkg?.path,
};
}
private extractSnippet(text: string, word: string, contextLength: number = 100): string {
const lowerText = text.toLowerCase();
const pos = lowerText.indexOf(word.toLowerCase());
if (pos === -1) return text.substring(0, contextLength) + '...';
const start = Math.max(0, pos - contextLength / 2);
const end = Math.min(text.length, pos + word.length + contextLength / 2);
let snippet = text.substring(start, end);
if (start > 0) snippet = '...' + snippet;
if (end < text.length) snippet = snippet + '...';
return snippet;
}
// ============ Package Path Helpers ============
getPackagePath(elementId: string): string {
const el = this.index.byId.get(elementId);
if (!el) return '';
const pkg = el.parentId ? this.index.packages.get(el.parentId) : undefined;
return pkg?.path || '';
}
}