diff --git a/packages/oslo-converter-uml-ea/README.md b/packages/oslo-converter-uml-ea/README.md index d8c703d..d20fdae 100644 --- a/packages/oslo-converter-uml-ea/README.md +++ b/packages/oslo-converter-uml-ea/README.md @@ -15,6 +15,5 @@ The service is executed from the CLI and expects the following parameters: | `--umlFile` | The URL or local file path of an EA UML diagram | :heavy_check_mark: || | `--diagramName` | The name of the UML diagram within the EAP file | :heavy_check_mark: || | `--outputFile` | The name of the RDF output file | No, but if omitted, output is written to process.stdout || -| `--specificationType` | The type of the specification | :heavy_check_mark: | `ApplicationProfile` or `Vocabulary` | | `--versionId` | Version identifier for the document | :heavy_check_mark: || | `--outputFormat` | RDF content-type specifiying the output format | :heavy_check_mark: | `application/ld+json` | \ No newline at end of file diff --git a/packages/oslo-converter-uml-ea/lib/config/DependencyInjectionConfig.ts b/packages/oslo-converter-uml-ea/lib/config/DependencyInjectionConfig.ts index d1ffed8..3bb57e7 100644 --- a/packages/oslo-converter-uml-ea/lib/config/DependencyInjectionConfig.ts +++ b/packages/oslo-converter-uml-ea/lib/config/DependencyInjectionConfig.ts @@ -29,38 +29,38 @@ container container .bind( - EaUmlConverterServiceIdentifier.OutputHandlerService - ) + EaUmlConverterServiceIdentifier.OutputHandlerService, +) .to(OutputHandlerService); container .bind>( - EaUmlConverterServiceIdentifier.ConverterHandler - ) + EaUmlConverterServiceIdentifier.ConverterHandler, +) .to(PackageConverterHandler) .inSingletonScope() .whenTargetNamed('PackageConverterHandler'); container .bind>( - EaUmlConverterServiceIdentifier.ConverterHandler - ) + EaUmlConverterServiceIdentifier.ConverterHandler, +) .to(AttributeConverterHandler) .inSingletonScope() .whenTargetNamed('AttributeConverterHandler'); container .bind>( - EaUmlConverterServiceIdentifier.ConverterHandler - ) + EaUmlConverterServiceIdentifier.ConverterHandler, +) .to(ElementConverterHandler) .inSingletonScope() .whenTargetNamed('ElementConverterHandler'); container .bind>( - EaUmlConverterServiceIdentifier.ConverterHandler - ) + EaUmlConverterServiceIdentifier.ConverterHandler, +) .to(ConnectorConverterHandler) .inSingletonScope() .whenTargetNamed('ConnectorConverterHandler'); diff --git a/packages/oslo-converter-uml-ea/lib/config/EaUmlConverterConfiguration.ts b/packages/oslo-converter-uml-ea/lib/config/EaUmlConverterConfiguration.ts index 6aebb1d..931e27d 100644 --- a/packages/oslo-converter-uml-ea/lib/config/EaUmlConverterConfiguration.ts +++ b/packages/oslo-converter-uml-ea/lib/config/EaUmlConverterConfiguration.ts @@ -13,13 +13,6 @@ export class EaUmlConverterConfiguration implements IConfiguration { */ private _diagramName: string | undefined; - /** - * Type of the specification for the intermediary RDF (JSON-LD) file. - * Mainly used to extract the correct labels from tags in the UML diagram - * Possible values are "ApplicationProfile" or "Vocabulary" - */ - private _specificationType: string | undefined; - /** * Name of the RDF output file */ @@ -43,7 +36,6 @@ export class EaUmlConverterConfiguration implements IConfiguration { public async createFromCli(params: YargsParams): Promise { this._umlFile = params.umlFile; this._diagramName = params.diagramName; - this._specificationType = params.specificationType; this._outputFile = (params.outputFile || 'report.jsonld'); this._versionId = params.versionId; this._outputFormat = params.outputFormat; @@ -64,13 +56,6 @@ export class EaUmlConverterConfiguration implements IConfiguration { return this._diagramName; } - public get specificationType(): string { - if (!this._specificationType) { - throw new Error(`Trying to access property "specificationType" before it was set.`); - } - return this._specificationType; - } - public get outputFile(): string { if (!this._outputFile) { throw new Error(`Trying to access property "outputFile" before it was set.`); diff --git a/packages/oslo-converter-uml-ea/lib/converter-handlers/AttributeConverterHandler.ts b/packages/oslo-converter-uml-ea/lib/converter-handlers/AttributeConverterHandler.ts index 18954cd..11b4f4f 100644 --- a/packages/oslo-converter-uml-ea/lib/converter-handlers/AttributeConverterHandler.ts +++ b/packages/oslo-converter-uml-ea/lib/converter-handlers/AttributeConverterHandler.ts @@ -1,14 +1,18 @@ import { URL } from 'url'; -import type { QuadStore } from '@oslo-flanders/core'; -import { ns, PropertyType } from '@oslo-flanders/core'; -import type { DataRegistry, EaAttribute, EaElement } from '@oslo-flanders/ea-uml-extractor'; +import { PropertyType, QuadStore } from '@oslo-flanders/core'; +import { ns } from '@oslo-flanders/core'; +import type { + DataRegistry, + EaAttribute, + EaElement, +} from '@oslo-flanders/ea-uml-extractor'; import { ElementType } from '@oslo-flanders/ea-uml-extractor'; import type * as RDF from '@rdfjs/types'; import { inject, injectable } from 'inversify'; import { EaUmlConverterConfiguration } from '../config/EaUmlConverterConfiguration'; import { EaUmlConverterServiceIdentifier } from '../config/EaUmlConverterServiceIdentifier'; import { CasingTypes } from '../enums/CasingTypes'; -import { DataTypes } from '../enums/DataTypes'; +import { DataTypes, datatypeIdentifierToHash } from '../enums/DataTypes'; import { TagNames } from '../enums/TagNames'; import { ConverterHandler } from '../interfaces/ConverterHandler'; import type { UriRegistry } from '../UriRegistry'; @@ -16,22 +20,36 @@ import { convertToCase, extractUri, getTagValue, ignore } from '../utils/utils'; @injectable() export class AttributeConverterHandler extends ConverterHandler { - @inject(EaUmlConverterServiceIdentifier.Configuration) public readonly config!: EaUmlConverterConfiguration; + @inject(EaUmlConverterServiceIdentifier.Configuration) + public readonly config!: EaUmlConverterConfiguration; - public async filterIgnoredObjects(model: DataRegistry): Promise { - model.attributes = model.attributes.filter(x => !ignore(x)); + public async filterIgnoredObjects( + model: DataRegistry + ): Promise { + model.attributes = model.attributes.filter((x) => !ignore(x)); return model; } - public async convert(model: DataRegistry, uriRegistry: UriRegistry, store: QuadStore): Promise { + public async convert( + model: DataRegistry, + uriRegistry: UriRegistry, + store: QuadStore + ): Promise { // Only attributes of elements that are on the target diagram will be passed to the output handler // and attributes that have a domain that is not an enumeration - const enumerationClasses = model.elements.filter(x => x.type === ElementType.Enumeration); + const enumerationClasses = model.elements.filter( + (x) => x.type === ElementType.Enumeration + ); model.attributes - .filter(x => model.targetDiagram.elementIds.includes(x.classId) && - !enumerationClasses.some(y => y.id === x.classId)) - .forEach(object => store.addQuads(this.createQuads(object, uriRegistry, model))); + .filter( + (x) => + model.targetDiagram.elementIds.includes(x.classId) && + !enumerationClasses.some((y) => y.id === x.classId) + ) + .forEach((object) => + store.addQuads(this.createQuads(object, uriRegistry, model)) + ); return store; } @@ -40,270 +58,340 @@ export class AttributeConverterHandler extends ConverterHandler { return model; } - public async assignUris(model: DataRegistry, uriRegistry: UriRegistry): Promise { + public async assignUris( + model: DataRegistry, + uriRegistry: UriRegistry + ): Promise { uriRegistry.attributeIdUriMap = new Map(); - model.attributes.forEach(attribute => { - const attributeClass = model.elements.find(x => x.id === attribute.classId); + model.attributes.forEach((attribute) => { + const attributeClass = model.elements.find( + (x) => x.id === attribute.classId + ); if (!attributeClass) { - throw new Error(`[AttributeConverterHandler]: Unable to find domain for attribute (${attribute.path}).`); + throw new Error( + `[AttributeConverterHandler]: Unable to find domain object for attribute (${attribute.path}).` + ); } - let attributeBaseUri: URL; - const packageTagValue = getTagValue(attribute, TagNames.DefiningPackage, null); + // We do not process attributes of enumerations + if (attributeClass.type === ElementType.Enumeration) { + return; + } + + const externalUri = getTagValue(attribute, TagNames.ExternalUri, null); + if (externalUri) { + uriRegistry.attributeIdUriMap.set(attribute.id, new URL(externalUri)); + } + + let attributeBaseURI: string | undefined; + const packageTagValue = getTagValue( + attribute, + TagNames.DefiningPackage, + null + ); if (packageTagValue) { - const referencedPackages = uriRegistry.packageNameToPackageMap.get(packageTagValue); + const packageObjects = + uriRegistry.packageNameToPackageMap.get(packageTagValue); - if (referencedPackages && referencedPackages.length > 1) { - this.logger.warn(`[AttributeConverterHandler]: Multiple packages discovered through name tag "${packageTagValue}" for attribute (${attribute.path}).`); + if (!packageObjects) { + throw new Error( + `[AttributeConverterHandler]: Package tag was defined, but unable to find a related package object for attribute (${attribute.path}).` + ); } - if (!referencedPackages) { - throw new Error(`[AttributeConverterHandler]: Package tag was defined, but unable to find a related package object for attribute (${attribute.path}).`); + if (packageObjects.length > 1) { + this.logger.warn( + `[AttributeConverterHandler]: Multiple packages discovered through name tag "${packageTagValue}" for attribute (${attribute.path}).` + ); } - attributeBaseUri = uriRegistry.packageIdUriMap.get(referencedPackages[0].packageId)!; - } else if (!uriRegistry.packageIdUriMap.has(attributeClass.packageId)) { - throw new Error(`[AttributeConverterHandler]: Unable to determine the package of attribute (${attribute.path}).`); + const packageURI = uriRegistry.packageIdUriMap.get( + packageObjects[0].packageId + ); + if (!packageURI) { + throw new Error( + `[AttributeConverterHandler]: Unable to find the URI of package (${packageObjects[0].parent}), but is needed as base URI.` + ); + } + attributeBaseURI = packageURI.toString(); } else { - attributeBaseUri = uriRegistry.packageIdUriMap.get(attributeClass.packageId)!; - } - - if (attributeClass.type === ElementType.Enumeration) { - let namespace = attributeBaseUri; + const packageURI = uriRegistry.packageIdUriMap.get( + attributeClass.packageId + ); - if (namespace.toString().endsWith('/') || namespace.toString().endsWith('#')) { - namespace = new URL(namespace.toString().slice(0, Math.max(0, namespace.toString().length - 1))); + if (!packageURI) { + throw new Error( + `[AttributeConverterHandler]: Unable to determine the package of attribute (${attribute.path}).` + ); } - let localName = getTagValue(attributeClass, TagNames.LocalName, attributeClass.name); - localName = convertToCase(localName); - - attributeBaseUri = new URL(`${namespace}/${localName}/`); + attributeBaseURI = packageURI.toString(); } - const attributeUri = extractUri(attribute, attributeBaseUri, CasingTypes.CamelCase); - uriRegistry.attributeIdUriMap.set(attribute.id, attributeUri); + let localName = getTagValue( + attribute, + TagNames.LocalName, + attribute.name + ); + localName = convertToCase(localName, CasingTypes.CamelCase); + + const attributeURI = new URL(`${attributeBaseURI}${localName}`); + uriRegistry.attributeIdUriMap.set(attribute.id, attributeURI); }); return uriRegistry; } - public createQuads(object: EaAttribute, uriRegistry: UriRegistry, model: DataRegistry): RDF.Quad[] { + public createQuads( + object: EaAttribute, + uriRegistry: UriRegistry, + model: DataRegistry + ): RDF.Quad[] { const quads: RDF.Quad[] = []; - const attributeInternalId = this.df.namedNode(`${this.baseUrnScheme}:${object.osloGuid}`); + const attributeInternalId = this.df.namedNode( + `${this.baseUrnScheme}:${object.osloGuid}` + ); const attributeUri = uriRegistry.attributeIdUriMap.get(object.id); if (!attributeUri) { - throw new Error(`[AttributeConverterHandler]: Unable to find URI for attribute (${object.path}).`); + throw new Error( + `[AttributeConverterHandler]: Unable to find URI for attribute (${object.path}).` + ); } const attributeUriNamedNode = this.df.namedNode(attributeUri.toString()); - let rangeUri = getTagValue(object, TagNames.Range, null); - let attributeType: PropertyType; - let rangeLabel: string | undefined; - let rangeElement: EaElement | undefined; - - // First, it is checked whether a 'range' tag has been defined on the attribute. - // If that was the case, then there should also be a 'literal' tag defined, that - // indicates whether the type of the attribute is a literal (DataTypeProperty) or - // not (ObjectProperty). Using 'range' tags always takes precedence - // If there was no range tag, the type of the attribute is checked in the list of - // (primitive) data types. - // Finally, it is checked whether the type of the attribute is the name of an EaElement. - // In that case, it can still be a DataTypeProperty, if a 'literal' tag is defined on the EaElement - // Otherwise, we just set the attribute type to the generic RDF Property - if (rangeUri) { - const isLiteral = getTagValue(object, TagNames.IsLiteral, false); - attributeType = isLiteral === 'true' ? PropertyType.DataTypeProperty : PropertyType.ObjectProperty; - } else if (DataTypes.has(object.type)) { - attributeType = PropertyType.DataTypeProperty; - rangeUri = DataTypes.get(object.type)!; - rangeLabel = object.type; - } else if (uriRegistry.elementNameToElementMap.has(object.type)) { - const elements = uriRegistry.elementNameToElementMap.get(object.type)!; - - if (elements.length > 1) { - // TODO: log message - } + quads.push( + this.df.quad( + attributeInternalId, + ns.oslo('assignedURI'), + attributeUriNamedNode + ) + ); - rangeElement = elements[0]; - const elementIsLiteral = getTagValue(rangeElement, TagNames.IsLiteral, false); + // Adding definitions, labels and usage notes + this.addEntityInformation(object, attributeInternalId, quads); - attributeType = elementIsLiteral === 'true' ? PropertyType.DataTypeProperty : PropertyType.ObjectProperty; - rangeUri = `${this.baseUrnScheme}:${rangeElement.osloGuid}`; - rangeLabel = object.type; - } else { - attributeType = PropertyType.Property; + const packageBaseUri = uriRegistry.packageIdUriMap.get( + model.targetDiagram.packageId + ); + + if (!packageBaseUri) { + throw new Error( + `[AttributeCOnverterHandler]: Unable to find the URI for the package in which the target diagram (${model.targetDiagram.name}) was placed.` + ); } - if (!rangeUri) { - throw new Error(`[AttributeConverterHandler]: Unable to get the URI for the range of attribute (${object.path}).`); + this.addScope( + object, + attributeInternalId, + packageBaseUri.toString(), + uriRegistry.attributeIdUriMap, + quads + ); + + // Determining the domain + const domainClass = model.elements.find((x) => x.id === object.classId); + if (!domainClass) { + throw new Error( + `[AttributeConverterHandler]: Unable to find the domain of attribute (${object.path}).` + ); } - const rangeUriNamedNode = this.df.namedNode(rangeUri); + const domainInternalId = this.df.namedNode( + `${this.baseUrnScheme}:${domainClass.osloGuid}` + ); quads.push( - this.df.quad( - attributeInternalId, - ns.rdf('type'), - this.df.namedNode(attributeType), - ), - this.df.quad( - attributeInternalId, - ns.example('assignedUri'), - attributeUriNamedNode, - ), - this.df.quad( - attributeInternalId, - ns.rdfs('range'), - rangeUriNamedNode, - ), + this.df.quad(attributeInternalId, ns.rdfs('domain'), domainInternalId) ); - quads.push(...this.addRangeRdfStatement(attributeInternalId, rangeUriNamedNode, model, uriRegistry, rangeLabel, rangeElement)); - - const definitionLiterals = this.getDefinition(object); - definitionLiterals.forEach(x => quads.push(this.df.quad(attributeInternalId, ns.rdfs('comment'), x))); + // Determining the range + let rangeURI = getTagValue(object, TagNames.Range, null); + let attributeType: PropertyType; - const labelLiterals = this.getLabel(object); - labelLiterals.forEach(x => quads.push(this.df.quad(attributeInternalId, ns.rdfs('label'), x))); + if (rangeURI) { + const rangeIsLiteral = getTagValue(object, TagNames.IsLiteral, false); + attributeType = + rangeIsLiteral === 'true' + ? PropertyType.DataTypeProperty + : PropertyType.ObjectProperty; + } else { + const rangeLabel = object.type; + attributeType = PropertyType.Property; - const usageNoteLiterals = this.getUsageNote(object); - usageNoteLiterals.forEach(x => quads.push(this.df.quad(attributeInternalId, ns.vann('usageNote'), x))); + const rangeElement = model.elements.find((x) => x.name === rangeLabel); - const domainClass = model.elements.find(x => x.id === object.classId); - if (domainClass) { - const domainInternalId = this.df.namedNode(`${this.baseUrnScheme}:${domainClass.osloGuid}`); + if (rangeElement) { + const rangeIsLiteral = getTagValue( + rangeElement, + TagNames.IsLiteral, + false + ); + attributeType = + rangeIsLiteral === 'true' + ? PropertyType.DataTypeProperty + : PropertyType.ObjectProperty; + rangeURI = `${this.baseUrnScheme}:${rangeElement.osloGuid}`; + + if (!model.targetDiagram.elementIds.includes(rangeElement.id)) { + quads.push( + ...this.getElementInformationQuads( + this.df.namedNode(rangeURI), + uriRegistry, + rangeElement + ) + ); + } + } else if (DataTypes.has(rangeLabel)) { + attributeType = PropertyType.DataTypeProperty; + const rangeAssignedURI = DataTypes.get(rangeLabel)!; + rangeURI = `${this.baseUrnScheme}:${datatypeIdentifierToHash( + rangeAssignedURI + )}`; - quads.push( - this.df.quad( - attributeInternalId, - ns.rdfs('domain'), - domainInternalId, - ), - ); + quads.push( + ...this.getDatatypeQuads( + this.df.namedNode(rangeURI), + this.df.namedNode(rangeAssignedURI), + rangeLabel + ) + ); + } } - const packageBaseUri = uriRegistry.packageIdUriMap.get(model.targetDiagram.packageId); - - if (!packageBaseUri) { - throw new Error(`[AttributeCOnverterHandler]: Unable to find the URI for the package in which the target diagram (${model.targetDiagram.name}) was placed.`); + if (!rangeURI) { + throw new Error( + `[AttributeConverterHandler]: Unable to get the URI for the range of attribute (${object.path}).` + ); } - const scope = this.getScope(object, packageBaseUri.toString(), uriRegistry.attributeIdUriMap); - quads.push(this.df.quad( - attributeInternalId, - ns.example('scope'), - this.df.literal(scope), - )); - quads.push( + this.df.quad( + attributeInternalId, + ns.rdfs('range'), + this.df.namedNode(rangeURI) + ), + this.df.quad( + attributeInternalId, + ns.rdf('type'), + this.df.namedNode(attributeType) + ), this.df.quad( attributeInternalId, ns.shacl('minCount'), - this.df.literal(object.lowerBound), + this.df.literal(object.lowerBound) ), this.df.quad( attributeInternalId, ns.shacl('maxCount'), - this.df.literal(object.upperBound), - ), + this.df.literal(object.upperBound) + ) ); - const parentUri = getTagValue(object, TagNames.ParentUri, null); - if (parentUri) { + // Add parent information + const parentURI = getTagValue(object, TagNames.ParentUri, null); + if (parentURI) { quads.push( this.df.quad( attributeInternalId, ns.rdfs('subPropertyOf'), - this.df.namedNode(parentUri), - ), + this.df.namedNode(parentURI) + ) ); } + // Add codelist information + const codelistURI = getTagValue(object, TagNames.ApCodelist, null); + if(codelistURI) { + quads.push( + this.df.quad( + attributeInternalId, + ns.oslo('codelist'), + this.df.namedNode(codelistURI) + ) + ) + } + return quads; } - private addRangeRdfStatement( - attributeUri: RDF.NamedNode, - rangeUri: RDF.NamedNode, - model: DataRegistry, - uriRegistry: UriRegistry, - rangeLabel?: string, - rangeElement?: EaElement, + private getDatatypeQuads( + rangeInternalId: RDF.NamedNode, + rangeAssignedURI: RDF.NamedNode, + rangeLabel: string ): RDF.Quad[] { const quads: RDF.Quad[] = []; - const statementBlankNode = this.df.blankNode(); - if (rangeLabel) { - quads.push( - this.df.quad( - statementBlankNode, - ns.rdfs('label'), - this.df.literal(rangeLabel), - ), - ); - } + quads.push( + this.df.quad(rangeInternalId, ns.rdf('type'), ns.rdfs('Datatype')), + this.df.quad(rangeInternalId, ns.oslo('assignedURI'), rangeAssignedURI), + this.df.quad( + rangeInternalId, + ns.oslo('diagramLabel'), + this.df.literal(rangeLabel) + ) + ); - if (rangeElement && - (!model.targetDiagram.elementIds.includes(rangeElement.id) || attributeUri.equals(ns.skos('Concept')))) { - const definitionValues = (this.getDefinition)(rangeElement); - definitionValues.forEach(x => quads.push(this.df.quad(statementBlankNode, ns.rdfs('comment'), x))); + return quads; + } - const usageNoteValues = (this.getUsageNote)(rangeElement); - usageNoteValues.forEach(x => quads.push(this.df.quad(statementBlankNode, ns.vann('usageNote'), x))); + private getElementInformationQuads( + rangeInternalId: RDF.NamedNode, + uriRegistry: UriRegistry, + rangeElement: EaElement + ): RDF.Quad[] { + const quads: RDF.Quad[] = []; + const referencedEntitiesGraph = this.df.namedNode('referencedEntities'); + + // TODO: switch to this.addEntityInformation? + this.addDefinitions( + rangeElement, + rangeInternalId, + referencedEntitiesGraph, + quads + ); + this.addLabels( + rangeElement, + rangeInternalId, + referencedEntitiesGraph, + quads + ); + this.addUsageNotes( + rangeElement, + rangeInternalId, + referencedEntitiesGraph, + quads + ); - const assignedUri = uriRegistry.elementIdUriMap.get(rangeElement.id); - if(!assignedUri){ - throw new Error(`[AttributeConverterHandler]: Unable to find the assigned URI for the range (${rangeElement.path}) of attribute.`); - } + const assignedUri = uriRegistry.elementIdUriMap.get(rangeElement.id); + if (!assignedUri) { + throw new Error( + `[AttributeConverterHandler]: Unable to find the assigned URI for the range (${rangeElement.path}) of attribute.` + ); + } - quads.push( - this.df.quad( - statementBlankNode, - ns.example('assignedUri'), - this.df.namedNode(assignedUri.toString()) - ) + quads.push( + this.df.quad( + rangeInternalId, + ns.oslo('assignedURI'), + this.df.namedNode(assignedUri.toString()), + referencedEntitiesGraph ) + ); - const skosCodelist = getTagValue(rangeElement, TagNames.ApCodelist, null); - if (skosCodelist) { - quads.push( - this.df.quad( - statementBlankNode, - ns.example('usesConceptScheme'), - this.df.namedNode(skosCodelist), - ), - ); - } - } - - if (quads.length > 0) { + const skosCodelist = getTagValue(rangeElement, TagNames.ApCodelist, null); + if (skosCodelist) { quads.push( this.df.quad( - statementBlankNode, - ns.rdf('type'), - ns.rdf('Statement'), - ), - this.df.quad( - statementBlankNode, - ns.rdf('subject'), - attributeUri, - ), - this.df.quad( - statementBlankNode, - ns.rdf('predicate'), - ns.rdfs('range'), - ), - this.df.quad( - statementBlankNode, - ns.rdf('object'), - rangeUri, - ), + rangeInternalId, + ns.oslo('codelist'), + this.df.namedNode(skosCodelist), + referencedEntitiesGraph + ) ); } diff --git a/packages/oslo-converter-uml-ea/lib/converter-handlers/ConnectorConverterHandler.ts b/packages/oslo-converter-uml-ea/lib/converter-handlers/ConnectorConverterHandler.ts index 9ebd6d8..734b12c 100644 --- a/packages/oslo-converter-uml-ea/lib/converter-handlers/ConnectorConverterHandler.ts +++ b/packages/oslo-converter-uml-ea/lib/converter-handlers/ConnectorConverterHandler.ts @@ -1,7 +1,16 @@ import type { QuadStore } from '@oslo-flanders/core'; import { ns } from '@oslo-flanders/core'; -import type { DataRegistry, EaConnector, EaElement, EaTag } from '@oslo-flanders/ea-uml-extractor'; -import { ConnectorType, NormalizedConnector, NormalizedConnectorTypes } from '@oslo-flanders/ea-uml-extractor'; +import type { + DataRegistry, + EaConnector, + EaElement, + EaTag, +} from '@oslo-flanders/ea-uml-extractor'; +import { + ConnectorType, + NormalizedConnector, + NormalizedConnectorTypes, +} from '@oslo-flanders/ea-uml-extractor'; import type * as RDF from '@rdfjs/types'; import { injectable } from 'inversify'; import { TagNames } from '../enums/TagNames'; @@ -11,143 +20,227 @@ import { convertToCase, getTagValue, ignore } from '../utils/utils'; @injectable() export class ConnectorConverterHandler extends ConverterHandler { - public async filterIgnoredObjects(model: DataRegistry): Promise { - model.connectors = model.connectors.filter(x => !ignore(x)); + public async filterIgnoredObjects( + model: DataRegistry + ): Promise { + model.connectors = model.connectors.filter((x) => !ignore(x)); return model; } - public async convert(model: DataRegistry, uriRegistry: UriRegistry, store: QuadStore): Promise { + public async convert( + model: DataRegistry, + uriRegistry: UriRegistry, + store: QuadStore + ): Promise { model.normalizedConnectors - .filter(x => model.targetDiagram.connectorsIds.includes(x.originalId)) - .forEach(object => store.addQuads(this.createQuads(object, uriRegistry, model))); + .filter((x) => model.targetDiagram.connectorsIds.includes(x.originalId)) + .forEach((object) => + store.addQuads(this.createQuads(object, uriRegistry, model)) + ); return store; } public async normalize(model: DataRegistry): Promise { model.normalizedConnectors = model.connectors - .filter(x => model.targetDiagram.connectorsIds.includes(x.id)) - .flatMap(x => this.normalizeConnector(x, model.elements)); + .filter((x) => model.targetDiagram.connectorsIds.includes(x.id)) + .flatMap((x) => this.normalizeConnector(x, model.elements)); return model; } - public async assignUris(model: DataRegistry, uriRegistry: UriRegistry): Promise { + public async assignUris( + model: DataRegistry, + uriRegistry: UriRegistry + ): Promise { uriRegistry.connectorOsloIdUriMap = new Map(); const diagramConnectors: NormalizedConnector[] = []; - model.targetDiagram.connectorsIds.forEach(connectorId => { - const filteredConnectors = model.normalizedConnectors.filter(x => x.originalId === connectorId) || []; + model.targetDiagram.connectorsIds.forEach((connectorId) => { + const filteredConnectors = + model.normalizedConnectors.filter( + (x) => x.originalId === connectorId + ) || []; diagramConnectors.push(...filteredConnectors); }); - diagramConnectors.forEach(connector => { + diagramConnectors.forEach((connector) => { // Inheritance related connectors do not get an URI. if (connector.originalType === ConnectorType.Generalization) { return; } - let connectorUri = getTagValue(connector, TagNames.ExternalUri, null); - const packageTagValue = getTagValue(connector, TagNames.DefiningPackage, null); - let definingPackageUri: URL | undefined; - - if (!connectorUri) { - // Here, we check the value of the 'package' tag. - // If there was no value, both source and destination should be defined in the same package. - // If there was a value, we check that the same package name is used for different packages, - // otherwise, we use the fallback uri - if (!packageTagValue) { - const sourcePackage = model.elements.find(x => x.id === connector.sourceObjectId); - const destinationPackage = model.elements.find(x => x.id === connector.destinationObjectId); - - if (sourcePackage && destinationPackage && sourcePackage.packageId === destinationPackage.packageId) { - definingPackageUri = uriRegistry.packageIdUriMap.get(sourcePackage.packageId)!; - } else { - this.logger.warn(`[ConnectorConverterHandler]: Can not determine the correct base URI for connector (${connector.path}) and the fallback URI (${uriRegistry.fallbackBaseUri}) will be assigned.`); - definingPackageUri = new URL(uriRegistry.fallbackBaseUri); - } - } else { - const packageObject = model.packages.find(x => x.name === packageTagValue); - if (!packageObject) { - throw new Error(`[ConnectorConverterHandler]: Unable to find package for name "${packageTagValue}".`); - } + const externalUri = getTagValue(connector, TagNames.ExternalUri, null); + if (externalUri) { + uriRegistry.connectorOsloIdUriMap.set( + connector.id, + new URL(externalUri) + ); + return; + } - definingPackageUri = new URL(uriRegistry.packageIdUriMap.get(packageObject.packageId)!); + const packageTagValue = getTagValue( + connector, + TagNames.DefiningPackage, + null + ); + let baseUri: string | undefined; + if (packageTagValue) { + const packageObject = model.packages.find( + (x) => x.name === packageTagValue + ); + + if (!packageObject) { + throw new Error( + `[ConnectorConverterHandler]: Unable to find package for name "${packageTagValue}".` + ); } - let localName = getTagValue(connector, TagNames.LocalName, connector.name); - localName = convertToCase(localName); - connectorUri = `${definingPackageUri}${localName}`; + const packageUri = uriRegistry.packageIdUriMap.get( + packageObject.packageId + ); + if (!packageUri) { + throw new Error( + `[ConnectorConverterHandler]: Unable to find the URI for package (${packageObject.path}).` + ); + } + + baseUri = packageUri.toString(); + } else { + const sourcePackage = model.elements.find( + (x) => x.id === connector.sourceObjectId + ); + const destinationPackage = model.elements.find( + (x) => x.id === connector.destinationObjectId + ); + + if ( + sourcePackage && + destinationPackage && + sourcePackage.packageId === destinationPackage.packageId + ) { + baseUri = uriRegistry.packageIdUriMap + .get(sourcePackage.packageId)! + .toString(); + } else { + this.logger.warn( + `[ConnectorConverterHandler]: Can not determine the correct base URI for connector (${connector.path}) and the fallback URI (${uriRegistry.fallbackBaseUri}) will be assigned.` + ); + baseUri = uriRegistry.fallbackBaseUri; + } } - uriRegistry.connectorOsloIdUriMap.set(connector.id, new URL(connectorUri)); + let localName = getTagValue( + connector, + TagNames.LocalName, + connector.name + ); + localName = convertToCase(localName); + const connectorUri = new URL(`${baseUri}${localName}`); + uriRegistry.connectorOsloIdUriMap.set( + connector.id, + connectorUri + ); }); return uriRegistry; } - public createQuads(object: NormalizedConnector, uriRegistry: UriRegistry, model: DataRegistry): RDF.Quad[] { + public createQuads( + object: NormalizedConnector, + uriRegistry: UriRegistry, + model: DataRegistry + ): RDF.Quad[] { const quads: RDF.Quad[] = []; - const connectorInternalId = this.df.namedNode(`${this.baseUrnScheme}:${object.osloGuid}`); + const connectorInternalId = this.df.namedNode( + `${this.baseUrnScheme}:${object.osloGuid}` + ); const connectorUri = uriRegistry.connectorOsloIdUriMap.get(object.id); if (!connectorUri) { - throw new Error(`[ConnectorConverterHandler]: Unable to find URI for connector (${object.path})`); + throw new Error( + `[ConnectorConverterHandler]: Unable to find URI for connector (${object.path})` + ); } const connectorUriNamedNode = this.df.namedNode(connectorUri.toString()); quads.push( - this.df.quad(connectorInternalId, ns.rdf('type'), ns.owl('ObjectProperty')), - this.df.quad(connectorInternalId, ns.example('assignedUri'), connectorUriNamedNode), + this.df.quad( + connectorInternalId, + ns.rdf('type'), + ns.owl('ObjectProperty') + ), + this.df.quad( + connectorInternalId, + ns.oslo('assignedURI'), + connectorUriNamedNode + ) ); - const definitionValues = this.getDefinition(object); - definitionValues.forEach(x => quads.push(this.df.quad(connectorInternalId, ns.rdfs('comment'), x))); + // Adding definitions, labels and usage notes + this.addEntityInformation(object, connectorInternalId, quads); - const labelValues = this.getLabel(object); - labelValues.forEach(x => quads.push(this.df.quad(connectorInternalId, ns.rdfs('label'), x))); - - const usageNoteValues = this.getUsageNote(object); - usageNoteValues.forEach(x => quads.push(this.df.quad(connectorInternalId, ns.vann('usageNote'), x))); + /*this.addDefinitions( + object, + connectorInternalId, + this.df.defaultGraph(), + quads + ); + this.addLabels(object, connectorInternalId, this.df.defaultGraph(), quads); + this.addUsageNotes( + object, + connectorInternalId, + this.df.defaultGraph(), + quads + );*/ - const domainObject = model.elements.find(x => x.id === object.sourceObjectId); + const domainObject = model.elements.find( + (x) => x.id === object.sourceObjectId + ); if (domainObject) { - const domainInternalId = this.df.namedNode(`${this.baseUrnScheme}:${domainObject.osloGuid}`); - quads.push(this.df.quad( - connectorInternalId, - ns.rdfs('domain'), - domainInternalId, - )); + const domainInternalId = this.df.namedNode( + `${this.baseUrnScheme}:${domainObject.osloGuid}` + ); + quads.push( + this.df.quad(connectorInternalId, ns.rdfs('domain'), domainInternalId) + ); } - const rangeObject = model.elements.find(x => x.id === object.destinationObjectId); + const rangeObject = model.elements.find( + (x) => x.id === object.destinationObjectId + ); if (rangeObject) { - const rangeInternalId = this.df.namedNode(`${this.baseUrnScheme}:${rangeObject.osloGuid}`); + const rangeInternalId = this.df.namedNode( + `${this.baseUrnScheme}:${rangeObject.osloGuid}` + ); - quads.push(this.df.quad( - connectorInternalId, - ns.rdfs('range'), - rangeInternalId, - )); + quads.push( + this.df.quad(connectorInternalId, ns.rdfs('range'), rangeInternalId) + ); } - const packageBaseUri = uriRegistry.packageIdUriMap.get(model.targetDiagram.packageId); + const packageBaseUri = uriRegistry.packageIdUriMap.get( + model.targetDiagram.packageId + ); if (!packageBaseUri) { - throw new Error(`[ConnectorConverterHandler]: Unnable to find URI for the package in which the target diagram (${model.targetDiagram.name}) was placed.`); + throw new Error( + `[ConnectorConverterHandler]: Unnable to find URI for the package in which the target diagram (${model.targetDiagram.name}) was placed.` + ); } - const scope = this.getScope(object, packageBaseUri.toString(), uriRegistry.connectorOsloIdUriMap); - quads.push(this.df.quad( + this.addScope( + object, connectorInternalId, - ns.example('scope'), - this.df.namedNode(scope), - )); + packageBaseUri.toString(), + uriRegistry.connectorOsloIdUriMap, + quads + ); let minCardinality; let maxCardinality; @@ -163,16 +256,18 @@ export class ConnectorConverterHandler extends ConverterHandler x.id === connector.sourceObjectId)!.name; - const destinationObjectName = elements.find(x => x.id === connector.destinationObjectId)!.name; - - normalizedConnectors.push(this.createNormalizedConnector( + normalizedConnectors.push( + this.createNormalizedConnector( connector, - `${sourceObjectName}.${connector.name}`, + connector.sourceRole, connector.destinationObjectId, connector.sourceObjectId, connector.sourceCardinality, - connector.tags, - )); + connector.sourceRoleTags + ) + ); + } - normalizedConnectors.push(this.createNormalizedConnector( + if (connector.destinationRole && connector.destinationRole !== '') { + normalizedConnectors.push( + this.createNormalizedConnector( connector, - `${destinationObjectName}.${connector.name}`, + connector.destinationRole, connector.sourceObjectId, connector.destinationObjectId, connector.destinationCardinality, - connector.tags, - )); - } else { - if (connector.sourceCardinality && connector.sourceCardinality !== '') { - normalizedConnectors.push(this.createNormalizedConnector( + connector.destinationRoleTags + ) + ); + } + + if (connector.name && connector.name !== '') { + if ( + connector.sourceCardinality && + connector.sourceCardinality !== '' && + connector.destinationCardinality && + connector.destinationCardinality !== '' + ) { + const sourceObjectName = elements.find( + (x) => x.id === connector.sourceObjectId + )!.name; + const destinationObjectName = elements.find( + (x) => x.id === connector.destinationObjectId + )!.name; + + normalizedConnectors.push( + this.createNormalizedConnector( connector, - connector.name, + `${sourceObjectName}.${connector.name}`, connector.destinationObjectId, connector.sourceObjectId, connector.sourceCardinality, - connector.tags, - )); - } + connector.tags + ) + ); - if (connector.destinationCardinality && connector.destinationCardinality !== '') { - normalizedConnectors.push(this.createNormalizedConnector( + normalizedConnectors.push( + this.createNormalizedConnector( connector, - connector.name, + `${destinationObjectName}.${connector.name}`, connector.sourceObjectId, connector.destinationObjectId, connector.destinationCardinality, - connector.tags, - )); + connector.tags + ) + ); + } else { + if (connector.sourceCardinality && connector.sourceCardinality !== '') { + normalizedConnectors.push( + this.createNormalizedConnector( + connector, + connector.name, + connector.destinationObjectId, + connector.sourceObjectId, + connector.sourceCardinality, + connector.tags + ) + ); + } + + if ( + connector.destinationCardinality && + connector.destinationCardinality !== '' + ) { + normalizedConnectors.push( + this.createNormalizedConnector( + connector, + connector.name, + connector.sourceObjectId, + connector.destinationObjectId, + connector.destinationCardinality, + connector.tags + ) + ); } } } if (connector.associationClassId) { - normalizedConnectors.push(...this.createNormalizedAssociationClassConnector(connector, elements)); + normalizedConnectors.push( + ...this.createNormalizedAssociationClassConnector(connector, elements) + ); } if (normalizedConnectors.length === 0) { @@ -286,7 +407,7 @@ export class ConnectorConverterHandler extends ConverterHandler x.id === connector.sourceObjectId); - const destinationObject = elements.find(x => x.id === connector.destinationObjectId); + const sourceObject = elements.find( + (x) => x.id === connector.sourceObjectId + ); + const destinationObject = elements.find( + (x) => x.id === connector.destinationObjectId + ); if (!sourceObject || !destinationObject) { // Log error return []; } - const assocationObject = elements.find(x => x.id === connector.associationClassId); + const assocationObject = elements.find( + (x) => x.id === connector.associationClassId + ); if (!assocationObject) { // TODO: log message or throw error return []; } - let sourceObjectIdentifier = `${assocationObject.name}.${convertToCase(sourceObject.name)}`; - let destinationObjectIdentifier = `${assocationObject.name}.${convertToCase(destinationObject.name)}`; + let sourceObjectIdentifier = `${assocationObject.name}.${convertToCase( + sourceObject.name + )}`; + let destinationObjectIdentifier = `${assocationObject.name}.${convertToCase( + destinationObject.name + )}`; let sourceLabel: string = sourceObjectIdentifier; let destinationLabel: string = destinationObjectIdentifier; @@ -331,29 +462,19 @@ export class ConnectorConverterHandler extends ConverterHandler { - public async filterIgnoredObjects(model: DataRegistry): Promise { + public async filterIgnoredObjects( + model: DataRegistry, + ): Promise { model.elements = model.elements.filter(x => !ignore(x)); return model; } - public async convert(model: DataRegistry, uriRegistry: UriRegistry, store: QuadStore): Promise { + public async convert( + model: DataRegistry, + uriRegistry: UriRegistry, + store: QuadStore, + ): Promise { // All elements will be processed and receive a URI, but only elements on the target diagram // will be passed to the OutputHandler. This flow is necessary because element types could be // in other packages and their URIs are needed to refer to in the output file. model.elements .filter(x => model.targetDiagram.elementIds.includes(x.id)) - .forEach(object => store.addQuads(this.createQuads(object, uriRegistry, model))); + .forEach(object => + store.addQuads(this.createQuads(object, uriRegistry, model))); return store; } @@ -33,7 +41,10 @@ export class ElementConverterHandler extends ConverterHandler { return model; } - public async assignUris(model: DataRegistry, uriRegistry: UriRegistry): Promise { + public async assignUris( + model: DataRegistry, + uriRegistry: UriRegistry, + ): Promise { uriRegistry.elementIdUriMap = new Map(); uriRegistry.elementNameToElementMap = new Map(); @@ -42,33 +53,46 @@ export class ElementConverterHandler extends ConverterHandler { if (externalUri) { uriRegistry.elementIdUriMap.set(element.id, new URL(externalUri)); - uriRegistry.elementNameToElementMap.set( - element.name, - [...uriRegistry.elementNameToElementMap.get(element.name) || [], element], - ); + uriRegistry.elementNameToElementMap.set(element.name, [ + ...uriRegistry.elementNameToElementMap.get(element.name) || [], + element, + ]); return; } let elementBaseUri: URL; - const packageTagValue = getTagValue(element, TagNames.DefiningPackage, null); + const packageTagValue = getTagValue( + element, + TagNames.DefiningPackage, + null, + ); if (packageTagValue) { - const referencedPackages = uriRegistry.packageNameToPackageMap.get(packageTagValue); + const referencedPackages = + uriRegistry.packageNameToPackageMap.get(packageTagValue); if (referencedPackages && referencedPackages.length > 1) { - this.logger.warn(`[ElementConverterHandler]: Multiple packages discovered through name tag "${packageTagValue}".`); + this.logger.warn( + `[ElementConverterHandler]: Multiple packages discovered through name tag "${packageTagValue}".`, + ); } if (!referencedPackages) { - throw new Error(`[ElementConverterHandler]: Package tag was defined, but unable to find the object for package ${packageTagValue}.`); + throw new Error( + `[ElementConverterHandler]: Package tag was defined, but unable to find the object for package ${packageTagValue}.`, + ); } - elementBaseUri = uriRegistry.packageIdUriMap.get(referencedPackages[0].packageId)!; + elementBaseUri = uriRegistry.packageIdUriMap.get( + referencedPackages[0].packageId, + )!; } else if (uriRegistry.packageIdUriMap.has(element.packageId)) { elementBaseUri = uriRegistry.packageIdUriMap.get(element.packageId)!; } else { - this.logger.warn(`[ElementConverterHandler]: Unable to find base URI for element (${element.path}).`); + this.logger.warn( + `[ElementConverterHandler]: Unable to find base URI for element (${element.path}).`, + ); elementBaseUri = new URL(uriRegistry.fallbackBaseUri); } @@ -77,86 +101,104 @@ export class ElementConverterHandler extends ConverterHandler { const elementUri = new URL(`${elementBaseUri}${localName}`); - uriRegistry.elementIdUriMap.set(element.id, new URL(elementUri)); - uriRegistry.elementNameToElementMap.set( - element.name, - [...uriRegistry.elementNameToElementMap.get(element.name) || [], element], - ); + uriRegistry.elementIdUriMap.set(element.id, elementUri); + uriRegistry.elementNameToElementMap.set(element.name, [ + ...uriRegistry.elementNameToElementMap.get(element.name) || [], + element, + ]); }); return uriRegistry; } - public createQuads(object: EaElement, uriRegistry: UriRegistry, model: DataRegistry): RDF.Quad[] { + public createQuads( + object: EaElement, + uriRegistry: UriRegistry, + model: DataRegistry, + ): RDF.Quad[] { const quads: RDF.Quad[] = []; - const objectInternalId = this.df.namedNode(`${this.baseUrnScheme}:${object.osloGuid}`); + const objectInternalId = this.df.namedNode( + `${this.baseUrnScheme}:${object.osloGuid}`, + ); const objectUri = uriRegistry.elementIdUriMap.get(object.id); if (!objectUri) { - throw new Error(`[ElementConverterHandler]: Unable to find URI for element (${object.path}).`); + throw new Error( + `[ElementConverterHandler]: Unable to find URI for element (${object.path}).`, + ); } const objectUriNamedNode = this.df.namedNode(objectUri.toString()); - // In case the URI is a skos:Concept, we do not publish - // its information. - // The only time information about a skos:Concept - // is published, is when an attribute its range - // is a skos:Concept. Then information about the skos:Concept - // is published as part of an rdf:Statement about the range - if (objectUriNamedNode.equals(ns.skos('Concept'))) { - return quads; - } - quads.push( - this.df.quad( - objectInternalId, - ns.example('assignedUri'), - objectUriNamedNode, - ), + this.df.quad(objectInternalId, ns.oslo('assignedURI'), objectUriNamedNode), ); - const definitionValues = this.getDefinition(object); - definitionValues.forEach(value => quads.push(this.df.quad(objectInternalId, ns.rdfs('comment'), value))); - - const usageNoteValues = this.getUsageNote(object); - usageNoteValues.forEach(value => quads.push(this.df.quad(objectInternalId, ns.vann('usageNote'), value))); - - const packageBaseUri = uriRegistry.packageIdUriMap.get(model.targetDiagram.packageId); + this.addDefinitions( + object, + objectInternalId, + this.df.defaultGraph(), + quads, + ); + this.addLabels(object, objectInternalId, this.df.defaultGraph(), quads); + this.addUsageNotes(object, objectInternalId, this.df.defaultGraph(), quads); + + // To be able to determine the scope of the element, + // we need to compare it to the base URI of the package + // which holds the target diagram. + const packageBaseUri = uriRegistry.packageIdUriMap.get( + model.targetDiagram.packageId, + ); if (!packageBaseUri) { - throw new Error(`[ElementConverterHandler]: Unnable to find URI for the package in which the target diagram (${model.targetDiagram.name}) was placed.`); + throw new Error( + `[ElementConverterHandler]: Unnable to find URI for the package in which the target diagram (${model.targetDiagram.name}) was placed.`, + ); } - const scope = this.getScope(object, packageBaseUri.toString(), uriRegistry.elementIdUriMap); - - quads.push(this.df.quad( + this.addScope( + object, objectInternalId, - ns.example('scope'), - this.df.literal(scope), - )); + packageBaseUri.toString(), + uriRegistry.elementIdUriMap, + quads, + ); + + quads.push( + ...this.getParentInformationQuads( + object, + objectInternalId, + uriRegistry, + model, + ), + ); + + quads.push(...this.getCodelistQuads(object, objectInternalId)); switch (object.type) { + case ElementType.Enumeration: case ElementType.Class: - quads.push(...this.createClassSpecificQuads(object, objectInternalId, uriRegistry, model)); + quads.push( + this.df.quad(objectInternalId, ns.rdf('type'), ns.owl('Class')), + ); break; case ElementType.DataType: - quads.push(...this.createDataTypeSpecificQuads(object, objectInternalId, uriRegistry, model)); - break; - - case ElementType.Enumeration: - quads.push(...this.createEnumerationSpecificQuads(object, objectInternalId, uriRegistry, model)); + quads.push( + this.df.quad(objectInternalId, ns.rdf('type'), ns.rdfs('Datatype')), + ); break; default: - throw new Error(`[ElementConverterHandler]: Object type (${object.type}) is not supported.`); + throw new Error( + `[ElementConverterHandler]: Object type (${object.type}) is not supported.`, + ); } return quads; } - private createClassSpecificQuads( + private getParentInformationQuads( object: EaElement, objectInternalId: RDF.NamedNode, uriRegistry: UriRegistry, @@ -164,130 +206,121 @@ export class ElementConverterHandler extends ConverterHandler { ): RDF.Quad[] { const quads: RDF.Quad[] = []; - quads.push( - this.df.quad( - objectInternalId, - ns.rdf('type'), - ns.owl('Class'), - ), - ); - - const labelLiterals = this.getLabel(object); - labelLiterals.forEach(x => quads.push(this.df.quad(objectInternalId, ns.rdfs('label'), x))); + // We search for a parent URI via the "parent" tag on the element + // TODO: it will be possible to set multiple parentURI tags + const parentURI = getTagValue(object, TagNames.ParentUri, null); + if (parentURI) { + quads.push( + this.df.quad( + objectInternalId, + ns.rdfs('subClassOf'), + this.df.namedNode(parentURI), + ), + ); + } + // We also search for parent relationships via connectors // Connectors array is used here, because NormalizedConnectors array doesn't have this type - const parentClassConnectors = model.connectors - .filter(x => x.type === ConnectorType.Generalization && x.sourceObjectId === object.id); + const parentClassConnectors = model.connectors.filter( + x => + x.type === ConnectorType.Generalization && + x.sourceObjectId === object.id, + ); parentClassConnectors.forEach(parentClassConnector => { - const parentClassObject = model.elements.find(x => x.id === parentClassConnector.destinationObjectId); + const parentClassObject = model.elements.find( + x => x.id === parentClassConnector.destinationObjectId, + ); if (!parentClassObject) { - throw new Error(`[ElementConverterHandler]: Unable to find parent class for class (${object.path}).`); + throw new Error( + `[ElementConverterHandler]: Unable to find parent object for class (${object.path}).`, + ); } - const parentInternalId = this.df.namedNode(`${this.baseUrnScheme}:${parentClassObject.osloGuid}`); + const parentInternalId = this.df.namedNode( + `${this.baseUrnScheme}:${parentClassObject.osloGuid}`, + ); quads.push( - this.df.quad( - objectInternalId, - ns.rdfs('subClassOf'), - parentInternalId, - ), + this.df.quad(objectInternalId, ns.rdfs('subClassOf'), parentInternalId), ); - if (!model.targetDiagram.connectorsIds.includes(parentClassConnector.id)) { - const parentAssignedUri = uriRegistry.elementIdUriMap.get(parentClassObject.id); + // In case the parent object is not visible on the target diagram + // we still add all the information, but put it in a different graph in the quad store + // so that we can separate it in the OSLO JSON-LD + if ( + !model.targetDiagram.connectorsIds.includes(parentClassConnector.id) + ) { + const parentAssignedUri = uriRegistry.elementIdUriMap.get( + parentClassObject.id, + ); if (!parentAssignedUri) { - throw new Error(`[ElementConverterHandler]: Unable to find the URI for parent of class (${object.path}).`); + throw new Error( + `[ElementConverterHandler]: Unable to find the assigned URI for parent of class (${object.path}).`, + ); } - const definitionValues = this.getDefinition(parentClassObject); - const labelValues = this.getLabel(parentClassObject); + const referencedEntitiesGraph = this.df.namedNode('referencedEntities'); + + this.addDefinitions( + parentClassObject, + parentInternalId, + referencedEntitiesGraph, + quads, + ); + + this.addLabels( + parentClassObject, + parentInternalId, + referencedEntitiesGraph, + quads, + ); + + this.addUsageNotes( + parentClassObject, + parentInternalId, + referencedEntitiesGraph, + quads, + ); - const statementBlankNode = this.df.blankNode(); quads.push( this.df.quad( - statementBlankNode, + parentInternalId, ns.rdf('type'), - ns.rdf('Statement'), - ), - this.df.quad( - statementBlankNode, - ns.rdf('subject'), - objectInternalId, + ns.owl('Class'), + referencedEntitiesGraph, ), this.df.quad( - statementBlankNode, - ns.rdf('predicate'), - ns.rdfs('subClassOf'), - ), - this.df.quad( - statementBlankNode, - ns.rdf('object'), parentInternalId, - ), - this.df.quad( - statementBlankNode, - ns.example('assignedUri'), + ns.oslo('assignedURI'), this.df.namedNode(parentAssignedUri.toString()), + referencedEntitiesGraph, ), ); - - definitionValues.forEach(x => quads.push(this.df.quad(statementBlankNode, ns.rdfs('comment'), x))); - labelValues.forEach(x => quads.push(this.df.quad(statementBlankNode, ns.rdfs('label'), x))); } }); return quads; } - private createDataTypeSpecificQuads( + private getCodelistQuads( object: EaElement, objectInternalId: RDF.NamedNode, - uriRegistry: UriRegistry, - model: DataRegistry, ): RDF.Quad[] { const quads: RDF.Quad[] = []; - quads.push( - this.df.quad( - objectInternalId, - ns.rdf('type'), - ns.rdfs('Datatype') - ), - ); - - const labelLiterals = this.getLabel(object); - labelLiterals.forEach(x => quads.push(this.df.quad(objectInternalId, ns.rdfs('label'), x))); - - return quads; - } + // Get codelist information via "ap-codelist" tag + const codelistUri = getTagValue(object, TagNames.ApCodelist, null); - private createEnumerationSpecificQuads( - object: EaElement, - objectInternalId: RDF.NamedNode, - uriRegistry: UriRegistry, - model: DataRegistry, - ): RDF.Quad[] { - const quads: RDF.Quad[] = []; - - quads.push( - this.df.quad( - objectInternalId, - ns.rdf('type'), - ns.owl('Class'), - ), - ); - - // FIXME: this should be available through a tag (language-aware) - const label = this.df.literal(object.name); - quads.push(this.df.quad(objectInternalId, ns.rdfs('label'), label)); - - const codelist = getTagValue(object, TagNames.ApCodelist, null); - - if (codelist) { - quads.push(this.df.quad(objectInternalId, ns.example('codelist'), this.df.namedNode(codelist))); + if (codelistUri) { + quads.push( + this.df.quad( + objectInternalId, + ns.oslo('codelist'), + this.df.namedNode(codelistUri), + ), + ); } return quads; diff --git a/packages/oslo-converter-uml-ea/lib/converter-handlers/PackageConverterHandler.ts b/packages/oslo-converter-uml-ea/lib/converter-handlers/PackageConverterHandler.ts index 0263f13..cd90125 100644 --- a/packages/oslo-converter-uml-ea/lib/converter-handlers/PackageConverterHandler.ts +++ b/packages/oslo-converter-uml-ea/lib/converter-handlers/PackageConverterHandler.ts @@ -75,16 +75,16 @@ export class PackageConverterHandler extends ConverterHandler { this.df.quad( packageInternalId, ns.rdf('type'), - ns.example('Package'), + ns.oslo('Package'), ), this.df.quad( packageInternalId, - ns.example('assignedUri'), + ns.oslo('assignedURI'), ontologyUriNamedNode, ), this.df.quad( packageInternalId, - ns.example('baseUri'), + ns.oslo('baseURI'), this.df.namedNode(baseUri.toString()), ), ); diff --git a/packages/oslo-converter-uml-ea/lib/enums/DataTypes.ts b/packages/oslo-converter-uml-ea/lib/enums/DataTypes.ts index 16f76c6..96ed762 100644 --- a/packages/oslo-converter-uml-ea/lib/enums/DataTypes.ts +++ b/packages/oslo-converter-uml-ea/lib/enums/DataTypes.ts @@ -24,3 +24,11 @@ export const DataTypes: Map = new Map( ['URI', `${xsd}#anyURI`], ], ); + +export const datatypeIdentifierToHash = (uri: string): number => { + let h: number = 0; + for (let i = 0; i < uri.length; i++) { + h = (Math.imul(31, h) + uri.charCodeAt(i)) | 0; + } + return h > 0 ? h : (h * - 1) +} diff --git a/packages/oslo-converter-uml-ea/lib/interfaces/ConverterHandler.ts b/packages/oslo-converter-uml-ea/lib/interfaces/ConverterHandler.ts index f99c049..c4af1b9 100644 --- a/packages/oslo-converter-uml-ea/lib/interfaces/ConverterHandler.ts +++ b/packages/oslo-converter-uml-ea/lib/interfaces/ConverterHandler.ts @@ -1,6 +1,11 @@ +import { URL } from 'url'; import type { QuadStore } from '@oslo-flanders/core'; -import { Logger, Scope } from '@oslo-flanders/core'; -import type { DataRegistry, EaObject, EaTag } from '@oslo-flanders/ea-uml-extractor'; +import { Logger, Scope, ns } from '@oslo-flanders/core'; +import type { + DataRegistry, + EaObject, + EaTag, +} from '@oslo-flanders/ea-uml-extractor'; import type * as RDF from '@rdfjs/types'; import { inject, injectable } from 'inversify'; import { DataFactory } from 'rdf-data-factory'; @@ -11,15 +16,23 @@ import type { UriRegistry } from '../UriRegistry'; @injectable() export abstract class ConverterHandler { - @inject(EaUmlConverterServiceIdentifier.Configuration) public readonly config!: EaUmlConverterConfiguration; - @inject(EaUmlConverterServiceIdentifier.Logger) public readonly logger!: Logger; + @inject(EaUmlConverterServiceIdentifier.Configuration) + public readonly config!: EaUmlConverterConfiguration; + + @inject(EaUmlConverterServiceIdentifier.Logger) + public readonly logger!: Logger; + public readonly df = new DataFactory(); public readonly baseUrnScheme: string = 'urn:oslo-toolchain'; /** * Creates RDF.Quads for objects with type T in the normalized model and adds them to an RDF.Store */ - public abstract convert(model: DataRegistry, uriRegistry: UriRegistry, store: QuadStore): Promise; + public abstract convert( + model: DataRegistry, + uriRegistry: UriRegistry, + store: QuadStore + ): Promise; /** * Normalizes objects with type T in the data model @@ -29,98 +42,233 @@ export abstract class ConverterHandler { /** * Assigns URIs to objects with type T in the data model and adds them to the URI registry */ - public abstract assignUris(normalizedModel: DataRegistry, uriRegistry: UriRegistry): Promise; + public abstract assignUris( + normalizedModel: DataRegistry, + uriRegistry: UriRegistry + ): Promise; /** * Creates and returns quads for an object with type T */ - public abstract createQuads(object: T, uriRegistry: UriRegistry, model?: DataRegistry): RDF.Quad[]; + public abstract createQuads( + object: T, + uriRegistry: UriRegistry, + model?: DataRegistry + ): RDF.Quad[]; /** * Removes objects that contain an ignore tag - * @param model + * @param model - The data registry containing the entities */ - public abstract filterIgnoredObjects(model: DataRegistry): Promise; + public abstract filterIgnoredObjects( + model: DataRegistry + ): Promise; - public getLabel(object: T): RDF.Literal[] { - const labelTags = this.config.specificationType === 'ApplicationProfile' ? - this.getLanguageDependentTag(object, TagNames.ApLabel, TagNames.Label) : - this.getLanguageDependentTag(object, TagNames.Label); + /** + * Adds information that was set via tags on an entity to the array of quads + * @param object - The entity to extract the information from and add it to the array of quads + * @param objectInternalId - An RDF.NamedNode representing the internal ID of the entity + * @param quads - The array of quads + * @param graph - The graph to add the quads to + */ + public addEntityInformation( + object: T, + objectInternalId: RDF.NamedNode, + quads: RDF.Quad[], + graph: RDF.Quad_Graph = this.df.defaultGraph(), + ): void { + this.addDefinitions(object, objectInternalId, graph, quads); + this.addLabels(object, objectInternalId, graph, quads); + this.addUsageNotes(object, objectInternalId, graph, quads); + } - return labelTags.length > 0 ? labelTags : [this.df.literal(object.name)]; + public addDefinitions( + object: T, + objectInternalId: RDF.NamedNode, + graph: RDF.Quad_Graph, + quads: RDF.Quad[], + ): void { + const apDefinitions = this.getTagValue(object, TagNames.ApDefinition); + this.addValuesToQuads( + apDefinitions, + objectInternalId, + ns.oslo('apDefinition'), + graph, + quads, + ); + + const vocDefinitions = this.getTagValue(object, TagNames.Definition); + this.addValuesToQuads( + vocDefinitions, + objectInternalId, + ns.oslo('vocDefinition'), + graph, + quads, + ); } - public getDefinition(object: T): RDF.Literal[] { - return this.config.specificationType === 'ApplicationProfile' ? - this.getLanguageDependentTag(object, TagNames.ApDefinition, TagNames.Definition) : - this.getLanguageDependentTag(object, TagNames.Definition); + public addLabels( + object: T, + objectInternalId: RDF.NamedNode, + graph: RDF.Quad_Graph, + quads: RDF.Quad[], + ): void { + const apLabels = this.getTagValue(object, TagNames.ApLabel); + this.addValuesToQuads( + apLabels, + objectInternalId, + ns.oslo('apLabel'), + graph, + quads, + ); + + const vocLabels = this.getTagValue(object, TagNames.Label); + this.addValuesToQuads( + vocLabels, + objectInternalId, + ns.oslo('vocLabel'), + graph, + quads, + ); + + // The name of the object as it appears on the diagram is also provided + this.addValuesToQuads( + [this.df.literal(object.name)], + objectInternalId, + ns.oslo('diagramLabel'), + graph, + quads, + ); } - public getUsageNote(object: T): RDF.Literal[] { - return this.config.specificationType === 'ApplicationProfile' ? - this.getLanguageDependentTag(object, TagNames.ApUsageNote, TagNames.UsageNote) : - this.getLanguageDependentTag(object, TagNames.UsageNote); + public addUsageNotes( + object: T, + objectInternalId: RDF.NamedNode, + graph: RDF.Quad_Graph, + quads: RDF.Quad[], + ): void { + const apUsageNotes = this.getTagValue(object, TagNames.ApUsageNote); + this.addValuesToQuads( + apUsageNotes, + objectInternalId, + ns.oslo('apUsageNote'), + graph, + quads, + ); + + const vocUsageNotes = this.getTagValue(object, TagNames.UsageNote); + this.addValuesToQuads( + vocUsageNotes, + objectInternalId, + ns.oslo('vocUsageNote'), + graph, + quads, + ); } - // TODO: watch out for URI tags containing a data.vlaanderen URI - public getScope(object: T, packageBaseUri: string, idUriMap: Map): Scope { + /** + * Determines the scope of the assigned URI of an entity + * @param object - The entity to determine the scope for + * @param objectInternalId - An RDF.NamedNode representing the internal ID of the entity + * @param packageBaseUri - The base URI that has been set on the EA package + * @param idUriMap - A map containing the entity id and assigned URI + * @param quads - An array of RDF.Quads to add the quad to + */ + public addScope( + object: T, + objectInternalId: RDF.NamedNode, + packageBaseUri: string, + idUriMap: Map, + quads: RDF.Quad[], + ): void { const uri = idUriMap.get(object.id); + let scope: Scope = Scope.External; + if (!uri) { - // TODO: log message - return Scope.Undefined; + this.logger.warn( + `[ConverterHandler]: Unable to find the URI for object with path ${object.path}. Setting scope to "Undefined".`, + ); + scope = Scope.Undefined; } - if (uri.toString().startsWith(packageBaseUri)) { - return Scope.InPackage; + if (uri?.toString().startsWith(packageBaseUri)) { + scope = Scope.InPackage; } - if (uri.toString().startsWith(this.config.publicationEnvironment)) { - return Scope.InPublicationEnvironment; + if (uri?.toString().startsWith(this.config.publicationEnvironment)) { + scope = Scope.InPublicationEnvironment; } - return Scope.External; + quads.push( + this.df.quad(objectInternalId, ns.oslo('scope'), this.df.namedNode(scope)), + ); + } + + /** + * Extract the value for a tag + * @param object - The entity to extract the tag values from + * @param tag - The name of the tag to extract the value for + * @returns - An array of RDF.Literals + */ + private getTagValue(object: T, tag: TagNames): RDF.Literal[] { + return this.getLanguageDependentTag(object, tag); + } + + /** + * Adds RDF.Literals to an array of quads for a configurable subject and predicate + * @param values - An array of RDF.Literals + * @param objectInternalId - An RDF.NamedNode representing the URI of the subject + * @param predicate - An RDF.NamedNode representing the predicate + * @param graph - To graph to add to quads to + * @param quads - The array of quads + */ + private addValuesToQuads( + values: RDF.Literal[], + objectInternalId: RDF.NamedNode, + predicate: RDF.NamedNode, + graph: RDF.Quad_Graph, + quads: RDF.Quad[], + ): void { + values.forEach(value => { + quads.push(this.df.quad(objectInternalId, predicate, value, graph)); + }); } /** * Returns all values of tags that contain the tag name * @param object — The object to extract the tag values from - * @param name — The tag name - * @param fallbackTag — Depending on the specification type, a fallback tag will be used to get tag values - * When specification type is 'ApplicationProfile' and certain tags are not added, - * the vocabulary fallback tags are used + * @param name — The name of the tag */ - private getLanguageDependentTag(object: T, name: TagNames, fallbackTag?: TagNames): RDF.Literal[] { + private getLanguageDependentTag(object: T, name: TagNames): RDF.Literal[] { const tags = object.tags.filter((x: EaTag) => x.tagName.startsWith(name)); const literals: RDF.Literal[] = []; const languageToTagValueMap = new Map(); - if (tags.length === 0) { - // TODO: Log warning that primary tag choice is not available, and fallback will be applied - if (!fallbackTag) { - // TODO: Log error that there is no fallback anymore - return literals; - } - - return this.getLanguageDependentTag(object, fallbackTag); - } - tags.forEach((tag: EaTag) => { const parts = tag.tagName.split('-'); const languageCode = parts[parts.length - 1]; - if (languageToTagValueMap.has(languageCode)) { - // TODO: Log warning that object has multiple occurrences and will be overriden - } - const tagValue = tag.tagValue; if (!tagValue) { - // TODO: Log warning for empty field? + this.logger.warn( + `[ConverterHandler]: Entity with path ${object.path} has an empty value for tag ${tag.tagName}.`, + ); return; } - literals.push(this.df.literal(tagValue.trim(), languageCode)); + if (languageToTagValueMap.has(languageCode)) { + this.logger.warn( + `[ConverterHandler]: Entity with path ${object.path} has already a value for ${tag.tagName} in language ${languageCode}, but will be overwritten.`, + ); + } + + languageToTagValueMap.set(languageCode, tag.tagValue); + }); + + languageToTagValueMap.forEach((value, languageCode) => { + literals.push(this.df.literal(value.trim(), languageCode)); }); return literals; diff --git a/packages/oslo-converter-uml-ea/lib/output-handlers/JsonLdOutputHandler.ts b/packages/oslo-converter-uml-ea/lib/output-handlers/JsonLdOutputHandler.ts index 51a7c72..8c65738 100644 --- a/packages/oslo-converter-uml-ea/lib/output-handlers/JsonLdOutputHandler.ts +++ b/packages/oslo-converter-uml-ea/lib/output-handlers/JsonLdOutputHandler.ts @@ -2,17 +2,18 @@ import type { WriteStream } from 'fs'; import type { IOutputHandler, QuadStore } from '@oslo-flanders/core'; import { ns } from '@oslo-flanders/core'; import type * as RDF from '@rdfjs/types'; +import { DataFactory } from 'rdf-data-factory'; import { getOsloContext } from '../utils/osloContext'; export class JsonLdOutputHandler implements IOutputHandler { public async write(store: QuadStore, writeStream: any): Promise { - const [packages, classes, attributes, dataTypes, statements] = + const [packages, classes, attributes, dataTypes, referencedEntities] = await Promise.all([ this.getPackages(store), this.getClasses(store), this.getAttributes(store), this.getDatatypes(store), - this.getRdfStatements(store), + this.getReferencedEntities(store), ]); const document: any = {}; @@ -23,7 +24,7 @@ export class JsonLdOutputHandler implements IOutputHandler { document.classes = classes; document.attributes = attributes; document.datatypes = dataTypes; - document.statements = statements; + document.referencedEntities = referencedEntities; (writeStream).write(JSON.stringify(document, null, 2)); } @@ -44,49 +45,40 @@ export class JsonLdOutputHandler implements IOutputHandler { } private async getPackages(store: QuadStore): Promise { - const packageIds = store.findSubjects( - ns.rdf('type'), - ns.example('Package') - ); + const packageIds = store.findSubjects(ns.rdf('type'), ns.oslo('Package')); return packageIds.map((id) => { const packageQuads = store.findQuads(id, null, null); - const baseUriValue = packageQuads.find((x) => - x.predicate.equals(ns.example('baseUri')) + const baseURIValue = packageQuads.find((x) => + x.predicate.equals(ns.oslo('baseURI')) ); - if (!baseUriValue) { + if (!baseURIValue) { throw new Error( - `Unnable to find base URI for package with .well-known id ${id.value}` + `Unable to find base URI for package with internal id ${id.value}` ); } - const assignedUri = packageQuads.find((x) => - x.predicate.equals(ns.example('assignedUri')) + const assignedURI = packageQuads.find((x) => + x.predicate.equals(ns.oslo('assignedURI')) ); return { '@id': id.value, '@type': 'Package', - ...(assignedUri && { - assignedUri: assignedUri.object.value, + ...(assignedURI && { + assignedURI: assignedURI.object.value, }), - baseUri: baseUriValue.object.value, + baseURI: baseURIValue.object.value, }; }); } private async getClasses(store: QuadStore): Promise { const classIds = store.findSubjects(ns.rdf('type'), ns.owl('Class')); - return classIds.reduce((jsonLdClasses, subject) => { - const assignedUri = store.getAssignedUri(subject); - // TODO: logging should happen here - if (!assignedUri) { - return jsonLdClasses; - } + return classIds.reduce((jsonLdClasses, subject) => { + const assignedURI = store.getAssignedUri(subject); - // Classes with skos:Concept URI are not being published separately, but only - // as part of an attribute's range - if (assignedUri.equals(ns.skos('Concept'))) { + if (!assignedURI) { return jsonLdClasses; } @@ -94,25 +86,25 @@ export class JsonLdOutputHandler implements IOutputHandler { const labelQuads = store.getLabels(subject); const scopeQuad = store.getScope(subject); const parentQuads = store.getParentsOfClass(subject); + const codelist = store.getCodelist(subject); + const usageNoteQuads = store.getUsageNotes(subject); jsonLdClasses.push({ '@id': subject.value, '@type': 'Class', - ...(assignedUri && { - assignedUri: assignedUri.value, + ...(assignedURI && { + assignedURI: assignedURI.value, }), - definition: definitionQuads.map((x) => ({ - '@language': (x).language, - '@value': x.value, - })), - label: labelQuads.map((x) => ({ - '@language': (x).language, - '@value': x.value, - })), + ...this.mapLabels(labelQuads), + ...this.mapDefinitions(definitionQuads), + ...this.mapUsageNotes(usageNoteQuads), scope: scopeQuad?.value, ...(parentQuads.length > 0 && { parent: parentQuads.map((x) => ({ '@id': x.value })), }), + ...(codelist && { + codelist: codelist.value, + }), }); return jsonLdClasses; @@ -138,7 +130,7 @@ export class JsonLdOutputHandler implements IOutputHandler { ...objectPropertyAttributeIds, ...propertyAttributeIds, ].map((subject) => { - const assignedUri = store.getAssignedUri(subject); + const assignedURI = store.getAssignedUri(subject); const definitionQuads = store.getDefinitions(subject); const attributeTypeQuad = store.findObject(subject, ns.rdf('type')); @@ -152,31 +144,17 @@ export class JsonLdOutputHandler implements IOutputHandler { const parentQuad = store.getParentOfProperty(subject); const minCardinalityQuad = store.getMinCardinality(subject); const maxCardinalityQuad = store.getMaxCardinality(subject); + const codelist = store.getCodelist(subject); return { '@id': subject.value, '@type': attributeTypeQuad?.value, - ...(assignedUri && { - assignedUri: assignedUri.value, - }), - ...(labelQuads.length > 0 && { - label: labelQuads.map((x) => ({ - '@language': (x).language, - '@value': x.value, - })), - }), - ...(definitionQuads.length > 0 && { - definition: definitionQuads.map((x) => ({ - '@language': (x).language, - '@value': x.value, - })), - }), - ...(usageNoteQuads.length > 0 && { - usageNote: usageNoteQuads.map((x) => ({ - '@language': (x).language, - '@value': x.value, - })), + ...(assignedURI && { + assignedURI: assignedURI.value, }), + ...this.mapLabels(labelQuads), + ...this.mapDefinitions(definitionQuads), + ...this.mapUsageNotes(usageNoteQuads), ...(domainQuad && { domain: { '@id': domainQuad.value, @@ -195,6 +173,9 @@ export class JsonLdOutputHandler implements IOutputHandler { minCount: minCardinalityQuad?.value, maxCount: maxCardinalityQuad?.value, scope: scopeQuad?.value, + ...(codelist && { + codelist: codelist.value, + }), }; }); } @@ -202,7 +183,7 @@ export class JsonLdOutputHandler implements IOutputHandler { private async getDatatypes(store: QuadStore): Promise { const datatypeIds = store.findSubjects(ns.rdf('type'), ns.rdfs('Datatype')); return datatypeIds.map((subject) => { - const assignedUri = store.getAssignedUri(subject); + const assignedURI = store.getAssignedUri(subject); const definitionQuads = store.getDefinitions(subject); const labelQuads = store.getLabels(subject); const usageNoteQuads = store.getUsageNotes(subject); @@ -211,27 +192,12 @@ export class JsonLdOutputHandler implements IOutputHandler { return { '@id': subject.value, '@type': 'Datatype', - ...(assignedUri && { - assignedUri: assignedUri.value, - }), - ...(labelQuads.length > 0 && { - label: labelQuads.map((x) => ({ - '@language': (x).language, - '@value': x.value, - })), - }), - ...(definitionQuads.length > 0 && { - definition: definitionQuads.map((x) => ({ - '@language': (x).language, - '@value': x.value, - })), - }), - ...(usageNoteQuads.length > 0 && { - usageNote: usageNoteQuads.map((x) => ({ - '@language': (x).language, - '@value': x.value, - })), + ...(assignedURI && { + assignedURI: assignedURI.value, }), + ...this.mapLabels(labelQuads), + ...this.mapDefinitions(definitionQuads), + ...this.mapUsageNotes(usageNoteQuads), ...(scopeQuad && { scope: scopeQuad.value, }), @@ -239,67 +205,123 @@ export class JsonLdOutputHandler implements IOutputHandler { }); } - private async getRdfStatements(store: QuadStore): Promise { - const statementIds = store.findSubjects( - ns.rdf('type'), - ns.rdf('Statement') + private async getReferencedEntities(store: QuadStore): Promise { + const df = new DataFactory(); + const referencedEntitiesGraph = df.namedNode('referencedEntities'); + + const quads: RDF.Quad[] = store.findQuads( + null, + null, + null, + referencedEntitiesGraph ); - return statementIds.map((subject) => { - const statementSubject = store.findObject(subject, ns.rdf('subject')); - const statementPredicate = store.findObject(subject, ns.rdf('predicate')); - const statementObject = store.findObject(subject, ns.rdf('object')); - - const statementAssignedUri = store.findObject(subject, ns.example('assignedUri')); - const statementLabels = store.findObjects(subject, ns.rdfs('label')); - const statementDefinitions = store.findObjects( + + const subjects = new Set(quads.map((x) => x.subject)); + + const result: any[] = []; + subjects.forEach((subject) => { + const assignedURI = store.getAssignedUri( subject, - ns.rdfs('comment') + referencedEntitiesGraph ); - const statementUsageNotes = store.findObjects( + const subjectType = store.findObject( subject, - ns.vann('usageNote') + ns.rdf('type'), + referencedEntitiesGraph ); - const statementConceptScheme = store.findObject( + const definitions = store.getDefinitions( subject, - ns.example('usesConceptScheme') + referencedEntitiesGraph ); + const labels = store.getLabels(subject, referencedEntitiesGraph); + const usageNotes = store.getUsageNotes(subject, referencedEntitiesGraph); + const scope = store.getScope(subject, referencedEntitiesGraph); - return { - '@type': ns.rdf('Statement').value, - subject: { - '@id': statementSubject?.value, - }, - predicate: { - '@id': statementPredicate?.value, - }, - object: { - '@id': statementObject?.value, - }, - ...(statementAssignedUri && { - assignedUri: statementAssignedUri.value, - }), - ...(statementLabels.length > 0 && { - label: statementLabels.map((x) => ({ - '@language': (x).language, - '@value': x.value, - })), - }), - ...(statementDefinitions.length > 0 && { - definition: statementDefinitions.map((x) => ({ - '@language': (x).language, - '@value': x.value, - })), + result.push({ + '@id': subject.value, + ...(subjectType && { + '@type': subjectType.value, }), - ...(statementUsageNotes.length > 0 && { - usageNote: statementUsageNotes.map((x) => ({ - '@language': (x).language, - '@value': x.value, - })), + ...(assignedURI && { + assignedURI: assignedURI.value, }), - ...(statementConceptScheme && { - usesConceptScheme: statementConceptScheme.value, + ...this.mapLabels(labels), + ...this.mapDefinitions(definitions), + ...this.mapUsageNotes(usageNotes), + ...(scope && { + scope: scope.value, }), - }; + }); }); + return result; + } + + private mapLabels(labels: RDF.Quad[]): any { + const vocLabels = labels.filter((x) => + x.predicate.equals(ns.oslo('vocLabel')) + ); + const apLabels = labels.filter((x) => + x.predicate.equals(ns.oslo('apLabel')) + ); + const diagramLabels = labels.filter((x) => + x.predicate.equals(ns.oslo('diagramLabel')) + ); + + return { + ...(vocLabels.length > 0 && { + vocLabel: vocLabels.map((x) => this.mapToLiteral(x)), + }), + ...(apLabels.length > 0 && { + apLabel: apLabels.map((x) => this.mapToLiteral(x)), + }), + ...(diagramLabels.length > 0 && { + diagramLabel: diagramLabels.map((x) => this.mapToLiteral(x, false)), + }), + }; + } + + private mapDefinitions(definitions: RDF.Quad[]): any { + const vocDefinitions = definitions.filter((x) => + x.predicate.equals(ns.oslo('vocDefinition')) + ); + const apDefinitions = definitions.filter((x) => + x.predicate.equals(ns.oslo('apDefinition')) + ); + + return { + ...(vocDefinitions.length > 0 && { + vocDefinition: vocDefinitions.map((x) => this.mapToLiteral(x)), + }), + ...(apDefinitions.length > 0 && { + apDefinition: apDefinitions.map((x) => this.mapToLiteral(x)), + }), + }; + } + + private mapUsageNotes(usageNotes: RDF.Quad[]): any { + const vocUsageNotes = usageNotes.filter((x) => + x.predicate.equals(ns.oslo('vocUsageNote')) + ); + const apUsageNotes = usageNotes.filter((x) => + x.predicate.equals(ns.oslo('apUsageNote')) + ); + + return { + ...(vocUsageNotes.length > 0 && { + vocUsageNote: vocUsageNotes.map((x) => this.mapToLiteral(x)), + }), + ...(apUsageNotes.length > 0 && { + apUsageNote: apUsageNotes.map((x) => this.mapToLiteral(x)), + }), + }; + } + + private mapToLiteral(quad: RDF.Quad, addLanguage: boolean = true): any { + return { + ...(addLanguage && { + '@language': (quad.object).language, + }), + '@value': quad.object.value, + }; } } diff --git a/packages/oslo-converter-uml-ea/lib/utils/utils.ts b/packages/oslo-converter-uml-ea/lib/utils/utils.ts index 98c7845..2a115ce 100644 --- a/packages/oslo-converter-uml-ea/lib/utils/utils.ts +++ b/packages/oslo-converter-uml-ea/lib/utils/utils.ts @@ -1,3 +1,4 @@ +import { URL } from 'url'; import type { EaObject, EaTag } from '@oslo-flanders/ea-uml-extractor'; import { CasingTypes } from '../enums/CasingTypes'; import { TagNames } from '../enums/TagNames'; @@ -8,7 +9,11 @@ export function ignore(object: EaObject): boolean { return Boolean(ignoreObject); } -export function getTagValue(object: any, tagName: TagNames, _default: any): string { +export function getTagValue( + object: any, + tagName: TagNames, + _default: any, +): string { const tags = object.tags?.filter((x: EaTag) => x.tagName === tagName); if (!tags || tags.length === 0) { @@ -24,7 +29,11 @@ export function getTagValue(object: any, tagName: TagNames, _default: any): stri } // TODO: this function should be removed and logic should be part of connectors -export function extractUri(object: EaObject, packageUri: URL, casing?: CasingTypes): URL { +export function extractUri( + object: EaObject, + packageUri: URL, + casing?: CasingTypes, +): URL { const uri = getTagValue(object, TagNames.ExternalUri, null); if (uri) { @@ -73,8 +82,13 @@ export function convertToCase(text: string, casing?: CasingTypes): string { return casedText; } -export function getDefininingPackageUri(uriRegistry: UriRegistry, packageName: string, currentPackageUri: URL): URL { - const referencedPackages = uriRegistry.packageNameToPackageMap.get(packageName) || []; +export function getDefininingPackageUri( + uriRegistry: UriRegistry, + packageName: string, + currentPackageUri: URL, +): URL { + const referencedPackages = + uriRegistry.packageNameToPackageMap.get(packageName) || []; if (referencedPackages.length === 0) { // TODO: log messag @@ -93,10 +107,15 @@ function removeCaret(text: string): string { } function toPascalCase(text: string): string { - return text.replace(/(?:^\w|[A-Z]|\b\w)/gu, (word: string, index: number) => word.toUpperCase()).replace(/\s+/gu, ''); + return text + .replace(/(?:^\w|[A-Z]|\b\w)/gu, (word: string, index: number) => + word.toUpperCase()) + .replace(/\s+/gu, ''); } function toCamelCase(text: string): string { - return text.replace(/(?:^\w|[A-Z]|\b\w)/gu, (word: string, index: number) => - index === 0 ? word.toLowerCase() : word.toUpperCase()).replace(/\s+/gu, ''); + return text + .replace(/(?:^\w|[A-Z]|\b\w)/gu, (word: string, index: number) => + index === 0 ? word.toLowerCase() : word.toUpperCase()) + .replace(/\s+/gu, ''); } diff --git a/packages/oslo-converter-uml-ea/test/JsonLdOutputHandler.unit.test.ts b/packages/oslo-converter-uml-ea/test/JsonLdOutputHandler.unit.test.ts index c46ee69..b75f7ee 100644 --- a/packages/oslo-converter-uml-ea/test/JsonLdOutputHandler.unit.test.ts +++ b/packages/oslo-converter-uml-ea/test/JsonLdOutputHandler.unit.test.ts @@ -1,9 +1,9 @@ /** * @group unit */ +import { QuadStore, ns } from '@oslo-flanders/core'; import { DataFactory } from 'rdf-data-factory'; import { JsonLdOutputHandler } from '../lib/output-handlers/JsonLdOutputHandler'; -import { QuadStore, ns } from '@oslo-flanders/core'; import { getOsloContext } from '../lib/utils/osloContext'; describe('JsonLdOutputHandler', () => { @@ -46,7 +46,7 @@ describe('JsonLdOutputHandler', () => { await outputHandler.write(store, writeStream); expect(writeStream.write).toHaveBeenCalledWith( - JSON.stringify(document, null, 2) + JSON.stringify(document, null, 2), ); }); @@ -55,7 +55,7 @@ describe('JsonLdOutputHandler', () => { df.quad( df.namedNode('http://example.org/id/version/1'), ns.prov('generatedAtTime'), - df.literal(new Date().toISOString()) + df.literal(new Date().toISOString()), ), ]; @@ -70,8 +70,7 @@ describe('JsonLdOutputHandler', () => { it('should throw an error when the version id can not be found', async () => { expect(() => - (outputHandler).addDocumentInformation({}, store) - ).toThrowError(); + (outputHandler).addDocumentInformation({}, store)).toThrowError(); }); it('should get all packages from the quad store and return a JSON-LD object', async () => { @@ -79,17 +78,17 @@ describe('JsonLdOutputHandler', () => { df.quad( df.namedNode('http://example.org/id/package/A'), ns.rdf('type'), - ns.example('Package') + ns.example('Package'), ), df.quad( df.namedNode('http://example.org/id/package/A'), ns.example('baseUri'), - df.namedNode('http://example.org/ns/A#') + df.namedNode('http://example.org/ns/A#'), ), df.quad( df.namedNode('http://example.org/id/package/A'), ns.example('assignedUri'), - df.namedNode('http://example.org/ns/A') + df.namedNode('http://example.org/ns/A'), ), ]; @@ -111,18 +110,18 @@ describe('JsonLdOutputHandler', () => { df.quad( df.namedNode('http://example.org/id/package/A'), ns.rdf('type'), - ns.example('Package') + ns.example('Package'), ), df.quad( df.namedNode('http://example.org/id/package/A'), ns.example('assignedUri'), - df.namedNode('http://example.org/ns/A#') + df.namedNode('http://example.org/ns/A#'), ), ]; store.addQuads(quads); await expect( - (outputHandler).getPackages(store) + (outputHandler).getPackages(store), ).rejects.toThrowError(); }); @@ -131,32 +130,32 @@ describe('JsonLdOutputHandler', () => { df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.rdf('type'), - ns.owl('Class') + ns.owl('Class'), ), df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.example('assignedUri'), - df.namedNode('http://example.org/id/class/1') + df.namedNode('http://example.org/id/class/1'), ), df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.rdfs('label'), - df.literal('TestClass', 'en') + df.literal('TestClass', 'en'), ), df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.rdfs('comment'), - df.literal('A definition', 'en') + df.literal('A definition', 'en'), ), df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.example('scope'), - df.namedNode('http://example.org/id/scope/A') + df.namedNode('http://example.org/id/scope/A'), ), df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.rdfs('subClassOf'), - df.namedNode('http://example.org/id/class/B') + df.namedNode('http://example.org/id/class/B'), ), ]; @@ -194,7 +193,7 @@ describe('JsonLdOutputHandler', () => { df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.rdf('type'), - ns.owl('Class') + ns.owl('Class'), ), ]; @@ -210,12 +209,12 @@ describe('JsonLdOutputHandler', () => { df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.rdf('type'), - ns.owl('Class') + ns.owl('Class'), ), df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.example('assignedUri'), - ns.skos('Concept') + ns.skos('Concept'), ), ]; @@ -230,57 +229,57 @@ describe('JsonLdOutputHandler', () => { df.quad( df.namedNode('urn:oslo-toolchain:property:1'), ns.rdf('type'), - ns.owl('DatatypeProperty') + ns.owl('DatatypeProperty'), ), df.quad( df.namedNode('urn:oslo-toolchain:property:1'), ns.example('assignedUri'), - df.namedNode('http://example.org/id/property/1') + df.namedNode('http://example.org/id/property/1'), ), df.quad( df.namedNode('urn:oslo-toolchain:property:1'), ns.rdfs('comment'), - df.literal('A definition', 'en') + df.literal('A definition', 'en'), ), df.quad( df.namedNode('urn:oslo-toolchain:property:1'), ns.rdfs('label'), - df.literal('A label', 'en') + df.literal('A label', 'en'), ), df.quad( df.namedNode('urn:oslo-toolchain:property:1'), ns.vann('usageNote'), - df.literal('A usage note', 'en') + df.literal('A usage note', 'en'), ), df.quad( df.namedNode('urn:oslo-toolchain:property:1'), ns.rdfs('domain'), - df.namedNode('urn:oslo-toolchain:class:1') + df.namedNode('urn:oslo-toolchain:class:1'), ), df.quad( df.namedNode('urn:oslo-toolchain:property:1'), ns.rdfs('range'), - ns.rdfs('Literal') + ns.rdfs('Literal'), ), df.quad( df.namedNode('urn:oslo-toolchain:property:1'), ns.example('scope'), - df.namedNode('http://example.org/id/scope/A') + df.namedNode('http://example.org/id/scope/A'), ), df.quad( df.namedNode('urn:oslo-toolchain:property:1'), ns.shacl('maxCount'), - df.literal('1') + df.literal('1'), ), df.quad( df.namedNode('urn:oslo-toolchain:property:1'), ns.shacl('minCount'), - df.literal('1') + df.literal('1'), ), df.quad( df.namedNode('urn:oslo-toolchain:property:1'), ns.rdfs('subPropertyOf'), - df.namedNode('http://example.org/id/property/2') + df.namedNode('http://example.org/id/property/2'), ), ]; @@ -324,7 +323,7 @@ describe('JsonLdOutputHandler', () => { maxCount: '1', scope: 'http://example.org/id/scope/A', }), - ]) + ]), ); }); @@ -333,37 +332,37 @@ describe('JsonLdOutputHandler', () => { df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.rdf('type'), - ns.rdfs('Datatype') + ns.rdfs('Datatype'), ), df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.example('assignedUri'), - df.namedNode('http://example.org/id/dataType/1') + df.namedNode('http://example.org/id/dataType/1'), ), df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.rdfs('comment'), - df.literal('A definition', 'en') + df.literal('A definition', 'en'), ), df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.rdfs('label'), - df.literal('A label', 'en') + df.literal('A label', 'en'), ), df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.vann('usageNote'), - df.literal('A usage note', 'en') + df.literal('A usage note', 'en'), ), df.quad( df.namedNode('urn:oslo-toolchain:1'), ns.example('scope'), - df.namedNode('http://example.org/id/scope/A') + df.namedNode('http://example.org/id/scope/A'), ), ]; store.addQuads(quads); - const datatypeObjects = await(outputHandler).getDatatypes(store); + const datatypeObjects = await (outputHandler).getDatatypes(store); expect(datatypeObjects).toEqual( expect.arrayContaining([ @@ -391,7 +390,7 @@ describe('JsonLdOutputHandler', () => { ], scope: 'http://example.org/id/scope/A', }), - ]) + ]), ); }); @@ -400,42 +399,42 @@ describe('JsonLdOutputHandler', () => { df.quad( df.namedNode('urn:oslo-toolchain:statement:1'), ns.rdf('type'), - ns.rdf('Statement') + ns.rdf('Statement'), ), df.quad( df.namedNode('urn:oslo-toolchain:statement:1'), ns.rdf('subject'), - df.namedNode('urn:oslo-toolchain:attribute:2') + df.namedNode('urn:oslo-toolchain:attribute:2'), ), df.quad( df.namedNode('urn:oslo-toolchain:statement:1'), ns.rdf('predicate'), - ns.rdfs('domain') + ns.rdfs('domain'), ), df.quad( df.namedNode('urn:oslo-toolchain:statement:1'), ns.rdf('object'), - df.namedNode('urn:oslo-toolchain:class:2') + df.namedNode('urn:oslo-toolchain:class:2'), ), df.quad( df.namedNode('urn:oslo-toolchain:statement:1'), ns.rdfs('comment'), - df.literal('A definition', 'en') + df.literal('A definition', 'en'), ), df.quad( df.namedNode('urn:oslo-toolchain:statement:1'), ns.rdfs('label'), - df.literal('A label', 'en') + df.literal('A label', 'en'), ), df.quad( df.namedNode('urn:oslo-toolchain:statement:1'), ns.vann('usageNote'), - df.literal('A usage note', 'en') + df.literal('A usage note', 'en'), ), df.quad( df.namedNode('urn:oslo-toolchain:statement:1'), ns.example('usesConceptScheme'), - df.namedNode('http://example.org/id/conceptScheme/A') + df.namedNode('http://example.org/id/conceptScheme/A'), ), ]; @@ -475,7 +474,7 @@ describe('JsonLdOutputHandler', () => { ], usesConceptScheme: 'http://example.org/id/conceptScheme/A', }), - ]) + ]), ); }); }); diff --git a/packages/oslo-core/lib/store/QuadStore.ts b/packages/oslo-core/lib/store/QuadStore.ts index 6402d58..cd6c1c6 100644 --- a/packages/oslo-core/lib/store/QuadStore.ts +++ b/packages/oslo-core/lib/store/QuadStore.ts @@ -26,67 +26,104 @@ export class QuadStore { const textStream = require('streamify-string')(buffer.toString()); return new Promise((resolve, reject) => { - rdfParser.parse(textStream, { path: file }) + rdfParser + .parse(textStream, { path: file }) .on('data', (quad: RDF.Quad) => this.store.addQuad(quad)) .on('error', (error: unknown) => reject(error)) .on('end', () => resolve()); }); } - public findQuads(subject: RDF.Term | null, predicate: RDF.Term | null, object: RDF.Term | null): RDF.Quad[] { - return this.store.getQuads(subject, predicate, object, null); + public findQuads( + subject: RDF.Term | null, + predicate: RDF.Term | null, + object: RDF.Term | null, + graph: RDF.Term | null = null, + ): RDF.Quad[] { + return this.store.getQuads(subject, predicate, object, graph); } - public findQuad(subject: RDF.Term | null, predicate: RDF.Term | null, object: RDF.Term | null): RDF.Quad | undefined { - return this.findQuads(subject, predicate, object).shift(); + public findQuad( + subject: RDF.Term | null, + predicate: RDF.Term | null, + object: RDF.Term | null, + graph: RDF.Term | null = null, + ): RDF.Quad | undefined { + return this.findQuads(subject, predicate, object, graph).shift(); } - public findSubjects(predicate: RDF.Term, object: RDF.Term): RDF.Term[] { - return this.store.getSubjects(predicate, object, null); + public findSubjects( + predicate: RDF.Term, + object: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.Term[] { + return this.store.getSubjects(predicate, object, graph); } - public findSubject(predicate: RDF.Term, object: RDF.Term): RDF.Term | undefined { - return this.findSubjects(predicate, object).shift(); + public findSubject( + predicate: RDF.Term, + object: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.Term | undefined { + return this.findSubjects(predicate, object, graph).shift(); } - public findObjects(subject: RDF.Term, predicate: RDF.Term): RDF.Term[] { - return this.store.getObjects(subject, predicate, null); + public findObjects( + subject: RDF.Term, + predicate: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.Term[] { + return this.store.getObjects(subject, predicate, graph); } - public findObject(subject: RDF.Term, predicate: RDF.Term): RDF.Term | undefined { - return this.findObjects(subject, predicate).shift(); + public findObject( + subject: RDF.Term, + predicate: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.Term | undefined { + return this.findObjects(subject, predicate, graph).shift(); } /** - * Finds the subject where predicate is 'rdf:type' and object 'example:Package' + * Finds the subject where predicate is 'rdf:type' and object 'oslo:Package' * @returns a RDF.NamedNode or undefined */ public getPackageId(): RDF.NamedNode | undefined { - return this.store.getSubjects(ns.rdf('type'), ns.example('Package'), null).shift(); + return ( + this.store.getSubjects(ns.rdf('type'), ns.oslo('Package'), null).shift() + ); } /** * Finds all subjects where predicate is 'rdf:type' and object 'owl:Class' * @returns an array of RDF.NamedNodes */ - public getClassIds(): RDF.NamedNode[] { - return this.store.getSubjects(ns.rdf('type'), ns.owl('Class'), null); + public getClassIds(graph: RDF.Term | null = null): RDF.NamedNode[] { + return ( + this.store.getSubjects(ns.rdf('type'), ns.owl('Class'), graph) + ); } /** - * Finds all subjects where predicate is 'rdf:type' and object 'owl:DatatypeProperty' - * @returns an array of RDF.NamedNodes - */ - public getDatatypePropertyIds(): RDF.NamedNode[] { - return this.store.getSubjects(ns.rdf('type'), ns.owl('DatatypeProperty'), null); + * Finds all subjects where predicate is 'rdf:type' and object 'owl:DatatypeProperty' + * @returns an array of RDF.NamedNodes + */ + public getDatatypePropertyIds( + graph: RDF.Term | null = null, + ): RDF.NamedNode[] { + return ( + this.store.getSubjects(ns.rdf('type'), ns.owl('DatatypeProperty'), graph) + ); } /** - * Finds all subjects where predicate is 'rdf:type' and object 'owl:ObjectProperty' - * @returns an array of RDF.NamedNodes - */ - public getObjectPropertyIds(): RDF.NamedNode[] { - return this.store.getSubjects(ns.rdf('type'), ns.owl('ObjectProperty'), null); + * Finds all subjects where predicate is 'rdf:type' and object 'owl:ObjectProperty' + * @returns an array of RDF.NamedNodes + */ + public getObjectPropertyIds(graph: RDF.Term | null = null): RDF.NamedNode[] { + return ( + this.store.getSubjects(ns.rdf('type'), ns.owl('ObjectProperty'), graph) + ); } /** @@ -95,18 +132,44 @@ export class QuadStore { * @param store The quad store * @returns An RDF.NamedNode or undefined if not found */ - public getAssignedUri(subject: RDF.Term): RDF.NamedNode | undefined { - return this.store.getObjects(subject, ns.example('assignedUri'), null).shift(); + public getAssignedUri( + subject: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.NamedNode | undefined { + return ( + this.store.getObjects(subject, ns.oslo('assignedURI'), graph).shift() + ); } /** - * Find all rdfs:labels for a given subject - * @param subject The RDF.Term to find the rdfs:labels for + * Find all quads with a label predicate (vocLabel, apLabel and diagramLabel) for a given subject + * @param subject The RDF.Term to find the labels for * @param store A N3 quad store - * @returns An array of RDF.Literals + * @returns An array of RDF.Quads */ - public getLabels(subject: RDF.Term): RDF.Literal[] { - return this.store.getObjects(subject, ns.rdfs('label'), null); + public getLabels( + subject: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.Quad[] { + const vocLabel = this.store.getQuads( + subject, + ns.oslo('vocLabel'), + null, + graph, + ); + const apLabel = this.store.getQuads( + subject, + ns.oslo('apLabel'), + null, + graph, + ); + const diagramLabel = this.store.getQuads( + subject, + ns.oslo('diagramLabel'), + null, + graph, + ); + return vocLabel.concat(apLabel).concat(diagramLabel); } /** @@ -116,8 +179,14 @@ export class QuadStore { * @param language A language tag * @returns An RDF.Literal or undefined if not found */ - public getLabel(subject: RDF.Term, language?: string): RDF.Literal | undefined { - return this.getLabels(subject).find(x => x.language === (language || '')); + public getLabel( + subject: RDF.Term, + language?: string, + graph: RDF.Term | null = null, + ): RDF.Quad | undefined { + return this.getLabels(subject, graph).find( + x => (x.object).language === (language || ''), + ); } /** @@ -126,8 +195,26 @@ export class QuadStore { * @param store A N3 quad store * @returns An array of RDF.Literals */ - public getDefinitions(subject: RDF.Term): RDF.Literal[] { - return this.store.getObjects(subject, ns.rdfs('comment'), null); + public getDefinitions( + subject: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.Quad[] { + const vocDefinitions = this.store.getQuads( + subject, + ns.oslo('vocDefinition'), + null, + graph + ); + + const apDefinitions = this.store.getQuads( + subject, + ns.oslo('apDefinition'), + null, + graph + ); + return ( + vocDefinitions.concat(apDefinitions) + ); } /** @@ -137,8 +224,14 @@ export class QuadStore { * @param language A language tag * @returns An RDF.Literal or undefined if not found */ - public getDefinition(subject: RDF.Term, language?: string): RDF.Literal | undefined { - return this.getDefinitions(subject).find(x => x.language === (language || '')); + public getDefinition( + subject: RDF.Term, + language?: string, + graph: RDF.Term | null = null, + ): RDF.Quad | undefined { + return this.getDefinitions(subject, graph).find( + x => (x.object).language === (language || ''), + ); } /** @@ -147,8 +240,13 @@ export class QuadStore { * @param store A N3 quad store * @returns An RDF.Term or undefined if not found */ - public getRange(subject: RDF.Term): RDF.NamedNode | undefined { - return this.store.getObjects(subject, ns.rdfs('range'), null).shift(); + public getRange( + subject: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.NamedNode | undefined { + return ( + this.store.getObjects(subject, ns.rdfs('range'), graph).shift() + ); } /** @@ -157,8 +255,13 @@ export class QuadStore { * @param store A N3 quad store * @returns An RDF.Term or undefined if not found */ - public getDomain(subject: RDF.Term): RDF.NamedNode | undefined { - return this.store.getObjects(subject, ns.rdfs('domain'), null).shift(); + public getDomain( + subject: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.NamedNode | undefined { + return ( + this.store.getObjects(subject, ns.rdfs('domain'), graph).shift() + ); } /** @@ -167,8 +270,24 @@ export class QuadStore { * @param store A N3 quad store * @returns An array of RDF.Literals */ - public getUsageNotes(subject: RDF.Term): RDF.Literal[] { - return this.store.getObjects(subject, ns.vann('usageNote'), null); + public getUsageNotes( + subject: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.Quad[] { + const vocUsageNotes = this.store.getQuads( + subject, + ns.oslo('vocUsageNote'), + null, + graph + ); + + const apUsageNotes = this.store.getQuads( + subject, + ns.oslo('apUsageNote'), + null, + graph + ); + return vocUsageNotes.concat(apUsageNotes); } /** @@ -178,8 +297,14 @@ export class QuadStore { * @param language A language tag * @returns An RDF.Literal or undefined if not found */ - public getUsageNote(subject: RDF.Term, language?: string): RDF.Literal | undefined { - return this.getUsageNotes(subject).find(x => x.language === (language || '')); + public getUsageNote( + subject: RDF.Term, + language?: string, + graph: RDF.Term | null = null, + ): RDF.Quad | undefined { + return this.getUsageNotes(subject, graph).find( + x => (x.object).language === (language || ''), + ); } /** @@ -187,8 +312,13 @@ export class QuadStore { * @param subject The RDF.Term to find the scope for * @returns An RDF.NamedNode or undefined if not found */ - public getScope(subject: RDF.Term): RDF.NamedNode | undefined { - return this.store.getObjects(subject, ns.example('scope'), null).shift(); + public getScope( + subject: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.NamedNode | undefined { + return ( + this.store.getObjects(subject, ns.oslo('scope'), graph).shift() + ); } /** @@ -196,17 +326,27 @@ export class QuadStore { * @param subject The RDF.Term to find the shacl:minCardinaly for * @returns An RDF.Literal or undefined if not found */ - public getMinCardinality(subject: RDF.Term): RDF.Literal | undefined { - return this.store.getObjects(subject, ns.shacl('minCount'), null).shift(); + public getMinCardinality( + subject: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.Literal | undefined { + return ( + this.store.getObjects(subject, ns.shacl('minCount'), graph).shift() + ); } /** - * Finds the shacl:maxCardinality for a given subject - * @param subject The RDF.Term to find the shacl:maxCardinaly for - * @returns An RDF.Literal or undefined if not found - */ - public getMaxCardinality(subject: RDF.Term): RDF.Literal | undefined { - return this.store.getObjects(subject, ns.shacl('maxCount'), null).shift(); + * Finds the shacl:maxCardinality for a given subject + * @param subject The RDF.Term to find the shacl:maxCardinaly for + * @returns An RDF.Literal or undefined if not found + */ + public getMaxCardinality( + subject: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.Literal | undefined { + return ( + this.store.getObjects(subject, ns.shacl('maxCount'), graph).shift() + ); } /** @@ -215,8 +355,13 @@ export class QuadStore { * @param store A N3 quad store * @returns An array of RDF.Terms */ - public getParentsOfClass(subject: RDF.Term): RDF.NamedNode[] { - return this.store.getObjects(subject, ns.rdfs('subClassOf'), null); + public getParentsOfClass( + subject: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.NamedNode[] { + return ( + this.store.getObjects(subject, ns.rdfs('subClassOf'), graph) + ); } /** @@ -225,8 +370,22 @@ export class QuadStore { * @param store A N3 quad store * @returns An RDF.Term or undefined if not found */ - public getParentOfProperty(subject: RDF.Term): RDF.NamedNode | undefined { - return this.store.getObjects(subject, ns.rdfs('subPropertyOf'), null).shift(); + public getParentOfProperty( + subject: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.NamedNode | undefined { + return ( + this.store.getObjects(subject, ns.rdfs('subPropertyOf'), graph).shift() + ); + } + + public getCodelist( + subject: RDF.Term, + graph: RDF.Term | null = null, + ): RDF.NamedNode | undefined { + return ( + this.store.getObjects(subject, ns.oslo('codelist'), graph).shift() + ); } /** @@ -242,18 +401,39 @@ export class QuadStore { statementPredicate: RDF.Term, statementObject: RDF.Term, ): RDF.Term | undefined { - const statementIds = this.store.getSubjects(ns.rdf('type'), ns.rdf('Statement'), null); - const statementSubjectPredicateSubjects = this.store.getSubjects(ns.rdf('subject'), statementSubject, null); - const statementPredicatePredicateSubjects = this.store.getSubjects(ns.rdf('predicate'), statementPredicate, null); - const statementObjectPredicateSubjects = this.store.getSubjects(ns.rdf('object'), statementObject, null); + const statementIds = this.store.getSubjects( + ns.rdf('type'), + ns.rdf('Statement'), + null, + ); + const statementSubjectPredicateSubjects = this.store.getSubjects( + ns.rdf('subject'), + statementSubject, + null, + ); + const statementPredicatePredicateSubjects = this.store.getSubjects( + ns.rdf('predicate'), + statementPredicate, + null, + ); + const statementObjectPredicateSubjects = this.store.getSubjects( + ns.rdf('object'), + statementObject, + null, + ); const targetIds = statementIds - .filter(x => statementSubjectPredicateSubjects.some(y => y.value === x.value)) - .filter(x => statementPredicatePredicateSubjects.some(y => y.value === x.value)) - .filter(x => statementObjectPredicateSubjects.some(y => y.value === x.value)); + .filter(x => + statementSubjectPredicateSubjects.some(y => y.value === x.value)) + .filter(x => + statementPredicatePredicateSubjects.some(y => y.value === x.value)) + .filter(x => + statementObjectPredicateSubjects.some(y => y.value === x.value)); if (targetIds.length > 1) { - throw new Error(`Found multiple statements with subject "${statementSubject.value}", predicate "${statementPredicate.value}" and object "${statementObject.value}".`); + throw new Error( + `Found multiple statements with subject "${statementSubject.value}", predicate "${statementPredicate.value}" and object "${statementObject.value}".`, + ); } return targetIds.shift(); @@ -295,7 +475,7 @@ export class QuadStore { predicate: RDF.Term, object: RDF.Term, language: string, - ): RDF.Literal | undefined { + ): RDF.Quad | undefined { const statementId = this.getTargetStatementId(subject, predicate, object); if (!statementId) { @@ -320,7 +500,7 @@ export class QuadStore { predicate: RDF.Term, object: RDF.Term, language: string, - ): RDF.Literal | undefined { + ): RDF.Quad | undefined { const statementId = this.getTargetStatementId(subject, predicate, object); if (!statementId) { @@ -345,7 +525,7 @@ export class QuadStore { predicate: RDF.Term, object: RDF.Term, language: string, - ): RDF.Literal | undefined { + ): RDF.Quad | undefined { const statementId = this.getTargetStatementId(subject, predicate, object); if (!statementId) {