OWL2DTDL/Program.cs (891 lines of code) (raw):

using CommandLine; using Microsoft.Azure.DigitalTwins.Parser; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OWL2DTDL.VocabularyHelper; using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; using VDS.RDF; using VDS.RDF.JsonLd; using VDS.RDF.Nodes; using VDS.RDF.Ontology; using VDS.RDF.Parsing; using VDS.RDF.Writing; namespace OWL2DTDL { class Program { public class Options { [Option('n', "no-imports", Required = false, HelpText = "Sets program to not follow owl:Imports declarations.")] public bool NoImports { get; set; } [Option('f', "file-path", Required = true, HelpText = "The path to the on-disk root ontology file to translate.", SetName = "fileOntology")] public string FilePath { get; set; } [Option('u', "uri-path", Required = true, HelpText = "The URI of the root ontology file to translate.", SetName = "uriOntology")] public string UriPath { get; set; } [Option('o', "outputPath", Required = true, HelpText = "The directory in which to create DTDL models.")] public string OutputPath { get; set; } [Option('m', "merged-output", Required = false, HelpText = "Sets program to output one merged JSON-LD file for batch import into ADT.")] public bool MergedOutput { get; set; } [Option('i', "ignorefile", Required = false, HelpText = "Path to a CSV file, the first column of which lists (whole or partial) IRI:s that should be ignored by this tool and not translated into DTDL output.")] public string IgnoreFile { get; set; } [Option('s', "ontologySource", Required = false, HelpText = "An identifier for the ontology source; will be used to generate DTMI:s per the following design, where interfaceName is the local name of a translated OWL class, and ontologyName is the last segment of the translated class's namespace: <dtmi:digitaltwins:{ontologySource}:{ontologyName}:{interfaceName};1>.")] public string OntologySource { get; set; } } /// <summary> /// Custom comparer for Ontology objects, based on W3C OWL2 specification for version IRIs. /// See https://www.w3.org/TR/owl2-syntax/#Ontology_IRI_and_Version_IRI /// </summary> class OntologyComparer : IEqualityComparer<Ontology> { // Note use of .AbsoluteUri, since .NET Uri comparison does not take URI fragment into account public bool Equals(Ontology x, Ontology y) { return !x.HasVersionUri() && !y.HasVersionUri() && (x.GetUri().AbsoluteUri == y.GetUri().AbsoluteUri) || x.HasVersionUri() && y.HasVersionUri() && (x.GetUri().AbsoluteUri == y.GetUri().AbsoluteUri) && (x.GetVersionUri().AbsoluteUri == y.GetVersionUri().AbsoluteUri); } // Method borrowed from https://stackoverflow.com/a/263416 public int GetHashCode(Ontology x) { // Generate partial hashes from identify-carrying fields, i.e., ontology IRI // and version IRI; if no version IRI exists, default to partial hash of 0. int oidHash = x.GetUri().AbsoluteUri.GetHashCode(); int vidHash = x.HasVersionUri() ? x.GetVersionUri().AbsoluteUri.GetHashCode() : 0; // int hash = 23; hash = hash * 37 + oidHash; hash = hash * 37 + vidHash; return hash; } } // Configuration fields private static bool _noImports; private static bool _localOntology; private static string _ontologyPath; private static string _outputPath; private static bool _mergedOutput; private static string _ontologySource; /// <summary> /// The root ontology being parsed. /// </summary> private static Ontology rootOntology; /// <summary> /// The joint ontology graph into which all imported ontologies are merged /// and upon which this tool subsequently operates. /// </summary> private static readonly OntologyGraph _ontologyGraph = new OntologyGraph(); /// <summary> /// Set of mappings of URI namespaces to short names, used, e.g., to mint DTMIs /// </summary> private static readonly Dictionary<Uri, string> namespacePrefixes = new Dictionary<Uri, string>(); /// <summary> /// Set of imported ontology URIs. Used to avoid revisiting a URI more than once in LoadImport(). /// </summary> private static readonly HashSet<Uri> importedOntologyUris = new HashSet<Uri>(); /// <summary> /// URIs that will be ignored by this tool, parsed from CSV file using -i command line option /// </summary> private static readonly HashSet<string> ignoredUris = new HashSet<string>(); /// <summary> /// Mapping of QUDT units and quantity kinds to DTDL units and semantic types /// </summary> private static readonly Dictionary<Uri, Uri> semanticTypesMap = new Dictionary<Uri, Uri>() { { QUDT.UnitNS.A, DTDL.ampere }, { QUDT.UnitNS.CentiM, DTDL.centimetre }, { QUDT.UnitNS.DEG, DTDL.degreeOfArc }, { QUDT.UnitNS.DEG_C, DTDL.degreeCelsius }, { QUDT.UnitNS.HP, DTDL.horsepower }, { QUDT.UnitNS.HR, DTDL.hour }, { QUDT.UnitNS.KiloGM, DTDL.kilogram }, { QUDT.UnitNS.KiloGM_PER_HR, DTDL.kilogramPerHour }, { QUDT.UnitNS.KiloPA, DTDL.kilopascal }, { QUDT.UnitNS.KiloW, DTDL.kilowatt }, { QUDT.UnitNS.KiloW_HR, DTDL.kilowattHour }, { QUDT.UnitNS.L, DTDL.litre }, { QUDT.UnitNS.L_PER_SEC, DTDL.litrePerSecond }, { QUDT.UnitNS.LUX, DTDL.lux }, { QUDT.UnitNS.M, DTDL.metre }, { QUDT.UnitNS.MilliM, DTDL.millimetre }, { QUDT.UnitNS.MIN, DTDL.minute }, { QUDT.UnitNS.M_PER_SEC, DTDL.metrePerSecond }, { QUDT.UnitNS.N, DTDL.newton }, { QUDT.UnitNS.PSI, DTDL.poundPerSquareInch }, { QUDT.UnitNS.REV_PER_MIN, DTDL.revolutionPerMinute }, { QUDT.UnitNS.V, DTDL.volt }, { QUDT.UnitNS.W, DTDL.watt }, { QUDT.QuantityKindNS.AngularVelocity, DTDL.AngularVelocity }, { QUDT.QuantityKindNS.ElectricCurrent, DTDL.Current }, { QUDT.QuantityKindNS.Energy, DTDL.Energy }, { QUDT.QuantityKindNS.Illuminance, DTDL.Illuminance }, { QUDT.QuantityKindNS.Angle, DTDL.Angle }, { QUDT.QuantityKindNS.Voltage, DTDL.Voltage }, { QUDT.QuantityKindNS.Power, DTDL.Power }, { QUDT.QuantityKindNS.Mass, DTDL.Mass }, { QUDT.QuantityKindNS.Force, DTDL.Force }, //{ QUDT.QuantityKindNS.MassFlowRate, DTDL.MassFlowRate }, //{ QUDT.QuantityKindNS.Pressure, DTDL.Pressure }, //{ QUDT.QuantityKindNS.Length, DTDL.Length }, //{ QUDT.QuantityKindNS.Temperature, DTDL.Temperature }, { QUDT.QuantityKindNS.Time, DTDL.TimeSpan }, //{ QUDT.QuantityKindNS.Velocity, DTDL.Velocity }, //{ QUDT.QuantityKindNS.Volume, DTDL.Volume }, //{ QUDT.QuantityKindNS.VolumeFlowRate, DTDL.VolumeFlowRate } }; static void Main(string[] args) { Parser.Default.ParseArguments<Options>(args) .WithParsed(o => { _outputPath = o.OutputPath; _noImports = o.NoImports; _mergedOutput = o.MergedOutput; if (o.FilePath != null) { _localOntology = true; _ontologyPath = o.FilePath; } else { _localOntology = false; _ontologyPath = o.UriPath; } // Parse ignored namespaces from ignorefile if (o.IgnoreFile != null) { using (var reader = new StreamReader(o.IgnoreFile)) { while (!reader.EndOfStream) { var line = reader.ReadLine(); var values = line.Split(';'); ignoredUris.Add(values[0]); } } } if (o.OntologySource != null) { _ontologySource = o.OntologySource; } }) .WithNotParsed((errs) => { Environment.Exit(1); }); // Turn off caching UriLoader.CacheDuration = TimeSpan.MinValue; // Load ontology graph from local or remote path Console.WriteLine($"Loading {_ontologyPath}."); if (_localOntology) { FileLoader.Load(_ontologyGraph, _ontologyPath); } else { UriLoader.Load(_ontologyGraph, new Uri(_ontologyPath)); } // Get the main ontology defined in the graph and add it to the namespace mapper rootOntology = _ontologyGraph.GetOntology(); namespacePrefixes.Add(rootOntology.GetUri(), rootOntology.GetShortName()); // If configured for it, parse owl:Imports transitively if (!_noImports) { foreach (Ontology import in rootOntology.Imports) { LoadImport(import); } } // Execute the main logic that generates DTDL interfaces. GenerateInterfaces(); // Execute DTDL parser-based validation of generated interfaces. // TODO commented out due to incompatible DotNetRdf in Microsoft.Azure.DigitalTwins.Parser // ValidateInterfaces(); } /// <summary> /// Checks if a given Ontology Resource is in the ignored names list. /// </summary> /// <param name="resource">Resource to check</param> /// <returns>True iff the resource is ignored</returns> private static bool IsIgnored(OntologyResource resource) { foreach (string ignoredUri in ignoredUris) { string resourceUri = resource.GetUri().AbsoluteUri; if (resourceUri.Contains(ignoredUri)) { return true; } } return false; } /// <summary> /// Main method that traverses the sets of classes in the imported ontology graph and generates DTDL representations. /// </summary> private static void GenerateInterfaces() { // Working graph Graph dtdlModel = new Graph(); // A whole bunch of language definitions. // TODO Extract all of these (often reused) node definitions into statics. // RDF/OWL specs IUriNode rdfType = dtdlModel.CreateUriNode(UriFactory.Create(RdfSpecsHelper.RdfType)); // DTDL classes IUriNode dtdl_Interface = dtdlModel.CreateUriNode(DTDL.Interface); IUriNode dtdl_Property = dtdlModel.CreateUriNode(DTDL.Property); IUriNode dtdl_Relationship = dtdlModel.CreateUriNode(DTDL.Relationship); IUriNode dtdl_Telemetry = dtdlModel.CreateUriNode(DTDL.Telemetry); IUriNode dtdl_Component = dtdlModel.CreateUriNode(DTDL.Component); IUriNode dtdl_Enum = dtdlModel.CreateUriNode(DTDL.Enum); IUriNode dtdl_EnumValue = dtdlModel.CreateUriNode(DTDL.EnumValue); IUriNode dtdl_Map = dtdlModel.CreateUriNode(DTDL.Map); // DTDL properties IUriNode dtdl_contents = dtdlModel.CreateUriNode(DTDL.contents); IUriNode dtdl_name = dtdlModel.CreateUriNode(DTDL.name); IUriNode dtdl_displayName = dtdlModel.CreateUriNode(DTDL.displayName); IUriNode dtdl_properties = dtdlModel.CreateUriNode(DTDL.properties); IUriNode dtdl_mapKey = dtdlModel.CreateUriNode(DTDL.mapKey); IUriNode dtdl_mapValue = dtdlModel.CreateUriNode(DTDL.mapValue); IUriNode dtdl_extends = dtdlModel.CreateUriNode(DTDL.extends); IUriNode dtdl_maxMultiplicity = dtdlModel.CreateUriNode(DTDL.maxMultiplicity); IUriNode dtdl_minMultiplicity = dtdlModel.CreateUriNode(DTDL.minMultiplicity); IUriNode dtdl_target = dtdlModel.CreateUriNode(DTDL.target); IUriNode dtdl_schema = dtdlModel.CreateUriNode(DTDL.schema); IUriNode dtdl_valueSchema = dtdlModel.CreateUriNode(DTDL.valueSchema); IUriNode dtdl_writable = dtdlModel.CreateUriNode(DTDL.writable); IUriNode dtdl_enumValue = dtdlModel.CreateUriNode(DTDL.enumValue); IUriNode dtdl_enumValues = dtdlModel.CreateUriNode(DTDL.enumValues); // Used to sort JObjects for merged array output, if selected as runtime option Dictionary<JObject, int> interfaceDepths = new Dictionary<JObject, int>(); Console.WriteLine(); Console.WriteLine("Generating DTDL Interface declarations: "); // Start looping through named, non-deprecated, non-ignored classes foreach (OntologyClass oClass in _ontologyGraph.OwlClasses.Where(oClass => oClass.IsNamed() && !oClass.IsDeprecated() && !IsIgnored(oClass) && !oClass.SuperClasses.Any(parent => parent.IsNamed() && IsIgnored(parent)))) { // Create Interface string interfaceDtmi = GetDTMI(oClass); Console.WriteLine($"\t* {interfaceDtmi}"); IUriNode interfaceNode = dtdlModel.CreateUriNode(UriFactory.Create(interfaceDtmi)); dtdlModel.Assert(new Triple(interfaceNode, rdfType, dtdl_Interface)); // If there are rdfs:labels, use them for DTDL displayName if (oClass.Label.Any()) { dtdlModel.Assert(GetDtdlDisplayNameTriples(oClass, interfaceNode)); } // If there are rdfs:comments, generate and assert DTDL description triples from them if (oClass.Comment.Any()) { dtdlModel.Assert(GetDtdlDescriptionTriples(oClass, interfaceNode)); } // If the class is a top-level class, i.e., it has zero superclasses that are // not owl:Thing, implement a generic 'name' property and externalIds alignment property IEnumerable<OntologyClass> namedDirectSuperClasses = oClass.DirectSuperClasses.Where(superClass => superClass.IsNamed()); if (!namedDirectSuperClasses.Where(superClass => !superClass.IsOwlThing()).Any()) { // Create name property node and name IBlankNode namePropertyNode = dtdlModel.CreateBlankNode(); dtdlModel.Assert(new Triple(interfaceNode, dtdl_contents, namePropertyNode)); dtdlModel.Assert(new Triple(namePropertyNode, rdfType, dtdl_Property)); ILiteralNode namePropertyDtdlNameNode = dtdlModel.CreateLiteralNode("name"); dtdlModel.Assert(new Triple(namePropertyNode, dtdl_name, namePropertyDtdlNameNode)); // Name is string IUriNode namePropertySchemaNode = dtdlModel.CreateUriNode(DTDL._string); dtdlModel.Assert(new Triple(namePropertyNode, dtdl_schema, namePropertySchemaNode)); // Display name (of name property) is hardcoded to "name". ILiteralNode namePropertyDisplayNameNode = dtdlModel.CreateLiteralNode("name", "en"); dtdlModel.Assert(new Triple(namePropertyNode, dtdl_displayName, namePropertyDisplayNameNode)); // Name is writeable ILiteralNode namePropertyTrueNode = dtdlModel.CreateLiteralNode("true", new Uri(XmlSpecsHelper.XmlSchemaDataTypeBoolean)); dtdlModel.Assert(new Triple(namePropertyNode, dtdl_writable, namePropertyTrueNode)); // Create externalIds IBlankNode externalIds = dtdlModel.CreateBlankNode(); dtdlModel.Assert(new Triple(interfaceNode, dtdl_contents, externalIds)); dtdlModel.Assert(new Triple(externalIds, rdfType, dtdl_Property)); ILiteralNode externalIdsDtdlName = dtdlModel.CreateLiteralNode("externalIds"); dtdlModel.Assert(new Triple(externalIds, dtdl_name, externalIdsDtdlName)); // External ids is map IBlankNode schemaNode = dtdlModel.CreateBlankNode(); dtdlModel.Assert(new Triple(schemaNode, rdfType, dtdl_Map)); // Map key IBlankNode schemaMapKey = dtdlModel.CreateBlankNode(); dtdlModel.Assert(new Triple(schemaNode, dtdl_mapKey, schemaMapKey)); ILiteralNode schemaMapKeyName = dtdlModel.CreateLiteralNode("externalIdName"); dtdlModel.Assert(new Triple(schemaMapKey, dtdl_name, schemaMapKeyName)); IUriNode schemaMapKeySchema = dtdlModel.CreateUriNode(DTDL._string); dtdlModel.Assert(new Triple(schemaMapKey, dtdl_schema, schemaMapKeySchema)); // Map value IBlankNode schemaMapValue = dtdlModel.CreateBlankNode(); dtdlModel.Assert(new Triple(schemaNode, dtdl_mapValue, schemaMapValue)); ILiteralNode schemaMapValueName = dtdlModel.CreateLiteralNode("externalIdValue"); dtdlModel.Assert(new Triple(schemaMapValue, dtdl_name, schemaMapValueName)); IUriNode schemaMapValueSchema = dtdlModel.CreateUriNode(DTDL._string); dtdlModel.Assert(new Triple(schemaMapValue, dtdl_schema, schemaMapValueSchema)); dtdlModel.Assert(new Triple(externalIds, dtdl_schema, schemaNode)); // Display name of external ids is hardcoded to "External IDs". ILiteralNode externalIdsDisplayName = dtdlModel.CreateLiteralNode("External IDs", "en"); dtdlModel.Assert(new Triple(externalIds, dtdl_displayName, externalIdsDisplayName)); // Name is writeable ILiteralNode externalIdsTrue = dtdlModel.CreateLiteralNode("true", new Uri(XmlSpecsHelper.XmlSchemaDataTypeBoolean)); dtdlModel.Assert(new Triple(externalIds, dtdl_writable, externalIdsTrue)); // Create customTags IBlankNode customTags = dtdlModel.CreateBlankNode(); dtdlModel.Assert(new Triple(interfaceNode, dtdl_contents, customTags)); dtdlModel.Assert(new Triple(customTags, rdfType, dtdl_Property)); ILiteralNode customTagsDtdlName = dtdlModel.CreateLiteralNode("customTags"); dtdlModel.Assert(new Triple(customTags, dtdl_name, customTagsDtdlName)); // Custom tags is map IBlankNode customTagsSchemaNode = dtdlModel.CreateBlankNode(); dtdlModel.Assert(new Triple(customTagsSchemaNode, rdfType, dtdl_Map)); // Map key IBlankNode customTagsSchemaMapKey = dtdlModel.CreateBlankNode(); dtdlModel.Assert(new Triple(customTagsSchemaNode, dtdl_mapKey, customTagsSchemaMapKey)); ILiteralNode customTagsSchemaMapKeyName = dtdlModel.CreateLiteralNode("tagName"); dtdlModel.Assert(new Triple(customTagsSchemaMapKey, dtdl_name, customTagsSchemaMapKeyName)); IUriNode customTagsSchemaMapKeySchema = dtdlModel.CreateUriNode(DTDL._string); dtdlModel.Assert(new Triple(customTagsSchemaMapKey, dtdl_schema, customTagsSchemaMapKeySchema)); // Map value IBlankNode customTagsSchemaMapValue = dtdlModel.CreateBlankNode(); dtdlModel.Assert(new Triple(customTagsSchemaNode, dtdl_mapValue, customTagsSchemaMapValue)); ILiteralNode customTagsSchemaMapValueName = dtdlModel.CreateLiteralNode("tagValue"); dtdlModel.Assert(new Triple(customTagsSchemaMapValue, dtdl_name, customTagsSchemaMapValueName)); IUriNode customTagsSchemaMapValueSchema = dtdlModel.CreateUriNode(DTDL._string); dtdlModel.Assert(new Triple(customTagsSchemaMapValue, dtdl_schema, customTagsSchemaMapValueSchema)); dtdlModel.Assert(new Triple(customTags, dtdl_schema, customTagsSchemaNode)); // Display name of custom tags is hardcoded to "Custom Tags". ILiteralNode customTagsDisplayName = dtdlModel.CreateLiteralNode("Custom Tags", "en"); dtdlModel.Assert(new Triple(customTags, dtdl_displayName, customTagsDisplayName)); // Name is writeable ILiteralNode customTagsTrue = dtdlModel.CreateLiteralNode("true", new Uri(XmlSpecsHelper.XmlSchemaDataTypeBoolean)); dtdlModel.Assert(new Triple(customTags, dtdl_writable, customTagsTrue)); } // If the class has direct superclasses, implement DTDL extends (for at most two, see limitation in DTDL spec) IEnumerable<OntologyClass> namedSuperClasses = oClass.DirectSuperClasses.Where(superClass => superClass.IsNamed() && !superClass.IsOwlThing() && !superClass.IsDeprecated()); if (namedSuperClasses.Any()) { foreach (OntologyClass superClass in namedSuperClasses.Take(2)) { // Only include non-deprecated subclass relations IUriNode rdfsSubClassOf = _ontologyGraph.CreateUriNode(RDFS.subClassOf); if (PropertyAssertionIsDeprecated(oClass.GetUriNode(), rdfsSubClassOf, superClass.GetUriNode())) { continue; } string superInterfaceDTMI = GetDTMI(superClass); IUriNode superInterfaceNode = dtdlModel.CreateUriNode(UriFactory.Create(superInterfaceDTMI)); dtdlModel.Assert(new Triple(interfaceNode, dtdl_extends, superInterfaceNode)); } } // For any outgoing object properties from the class to other classes, create corresponding DTDL Relationships foreach (Relationship relationship in oClass.GetRelationships().Where(relationship => relationship.Property.IsObjectProperty() && !relationship.Property.IsDeprecated() && !relationship.Target.IsDeprecated() && !IsIgnored(relationship.Target) && !relationship.Target.SuperClasses.Any(parent => parent.IsNamed() && IsIgnored(parent)) && !IsIgnored(relationship.Property))) { OntologyProperty oProperty = relationship.Property; // Only include relationships with valid names string relationshipName = string.Concat(oProperty.GetLocalName().Take(64)); if (!IsCompliantDtdlName(relationshipName)) { Console.Error.WriteLine($"Unable to generate Relationship '{relationshipName}' on Interface '{interfaceDtmi}'; underlying property name does not adhere to DTDL regex."); continue; } // If we have seen this relationship linked from a subclass, skip it; DTDL does not allow subinterfaces // to specialise properties/relationships if (PropertyIsDefinedOnChildClass(oClass, oProperty)) { continue; } // Define the Relationship and its name IBlankNode relationshipNode = dtdlModel.CreateBlankNode(); dtdlModel.Assert(new Triple(interfaceNode, dtdl_contents, relationshipNode)); ILiteralNode relationshipNameNode = dtdlModel.CreateLiteralNode(relationshipName); dtdlModel.Assert(new Triple(relationshipNode, dtdl_name, relationshipNameNode)); // If there are rdfs:labels, use them for DTDL displayName if (oProperty.Label.Any()) { dtdlModel.Assert(GetDtdlDisplayNameTriples(oProperty, relationshipNode)); } // If there are rdfs:comments, generate and assert DTDL description triples from them if (oProperty.Comment.Any()) { dtdlModel.Assert(GetDtdlDescriptionTriples(oProperty, relationshipNode)); } // If this relationship is annotated with the dtdlType annotation, // and that type is a component, then assert the relationship is a component, set its schema, and go to next relationship. if (!relationship.Target.IsOwlThing() && relationship.Target.IsNamed() && relationship.Target.DtdlTypes().Contains("component")) { dtdlModel.Assert(new Triple(relationshipNode, rdfType, dtdl_Component)); string schemaDtmi = GetDTMI(relationship.Target); IUriNode schemaInterfaceNode = dtdlModel.CreateUriNode(UriFactory.Create(schemaDtmi)); dtdlModel.Assert(new Triple(relationshipNode, dtdl_schema, schemaInterfaceNode)); continue; } // Assert target (if defined) if (!relationship.Target.IsOwlThing()) { string targetDtmi = GetDTMI(relationship.Target); IUriNode targetInterfaceNode = dtdlModel.CreateUriNode(UriFactory.Create(targetDtmi)); dtdlModel.Assert(new Triple(relationshipNode, dtdl_target, targetInterfaceNode)); } // Assert that this is indeed a Relationship dtdlModel.Assert(new Triple(relationshipNode, rdfType, dtdl_Relationship)); // Assert min multiplicity. Hardcoded: per DTDL v2 spec: "For this release, minMultiplicity must always be 0" if (relationship.MinimumCount.HasValue) { ILiteralNode minMultiplicityNode = dtdlModel.CreateLiteralNode("0", UriFactory.Create(XmlSpecsHelper.XmlSchemaDataTypeInteger)); dtdlModel.Assert(new Triple(relationshipNode, dtdl_minMultiplicity, minMultiplicityNode)); } // Assert max multiplicity if (relationship.MaximumCount.HasValue) { ILiteralNode maxMultiplicityNode = dtdlModel.CreateLiteralNode(relationship.MaximumCount.Value.ToString(), UriFactory.Create(XmlSpecsHelper.XmlSchemaDataTypeInteger)); dtdlModel.Assert(new Triple(relationshipNode, dtdl_maxMultiplicity, maxMultiplicityNode)); } // Extract annotations on object properties -- these become DTDL Relationship Properties // We only support annotations w/ singleton ranges (though those singletons may be enumerations) IEnumerable<OntologyProperty> annotationsOnRelationship = _ontologyGraph.OwlAnnotationProperties .Where(annotationProp => annotationProp.Ranges.Count() == 1) .Where(annotationProp => annotationProp.Domains.Select(annotationDomain => annotationDomain.Resource).Contains(oProperty.Resource)); foreach (OntologyProperty annotationProperty in annotationsOnRelationship) { // Define nested Property and its name IBlankNode nestedPropertyNode = dtdlModel.CreateBlankNode(); dtdlModel.Assert(new Triple(nestedPropertyNode, rdfType, dtdl_Property)); dtdlModel.Assert(new Triple(relationshipNode, dtdl_properties, nestedPropertyNode)); string nestedPropertyName = string.Concat(annotationProperty.GetLocalName().Take(64)); ILiteralNode nestedPropertyNameNode = dtdlModel.CreateLiteralNode(nestedPropertyName); dtdlModel.Assert(new Triple(nestedPropertyNode, dtdl_name, nestedPropertyNameNode)); // Assert that the nested property is writable ILiteralNode trueNode = dtdlModel.CreateLiteralNode("true", new Uri(XmlSpecsHelper.XmlSchemaDataTypeBoolean)); dtdlModel.Assert(new Triple(nestedPropertyNode, dtdl_writable, trueNode)); // If there are rdfs:labels, use them for DTDL displayName if (annotationProperty.Label.Any()) { dtdlModel.Assert(GetDtdlDisplayNameTriples(annotationProperty, nestedPropertyNode)); } // If there are rdfs:comments, generate and assert DTDL description triples from them if (oProperty.Comment.Any()) { dtdlModel.Assert(GetDtdlDescriptionTriples(annotationProperty, nestedPropertyNode)); } // Set range OntologyClass annotationPropertyRange = annotationProperty.Ranges.First(); HashSet<Triple> schemaTriples = GetDtdlTriplesForRange(annotationPropertyRange, nestedPropertyNode); dtdlModel.Assert(schemaTriples); } } // For any outgoing data properties from the class to datatypes, create corresponding DTDL Properties foreach (Relationship relationship in oClass.GetRelationships().Where(relationship => relationship.Property.IsDataProperty() && !relationship.Property.IsDeprecated() && !IsIgnored(relationship.Property))) { OntologyProperty oProperty = relationship.Property; // Only include properties with valid names string propertyName = string.Concat(oProperty.GetLocalName().Take(64)); if (!IsCompliantDtdlName(propertyName)) { Console.Error.WriteLine($"Unable to generate Property '{propertyName}' on Interface '{interfaceDtmi}'; underlying property name does not adhere to DTDL regex."); continue; } // If we have seen this relationship linked from a subclass, skip it; DTDL does not allow subinterfaces // to specialise properties/relationships if (PropertyIsDefinedOnChildClass(oClass, oProperty)) { continue; } // Create Property node and name IBlankNode propertyNode = dtdlModel.CreateBlankNode(); dtdlModel.Assert(new Triple(interfaceNode, dtdl_contents, propertyNode)); dtdlModel.Assert(new Triple(propertyNode, rdfType, dtdl_Property)); ILiteralNode propertyNameNode = dtdlModel.CreateLiteralNode(propertyName); dtdlModel.Assert(new Triple(propertyNode, dtdl_name, propertyNameNode)); // Assert that the property is writable ILiteralNode trueNode = dtdlModel.CreateLiteralNode("true", new Uri(XmlSpecsHelper.XmlSchemaDataTypeBoolean)); dtdlModel.Assert(new Triple(propertyNode, dtdl_writable, trueNode)); // Extract and populate schema HashSet<Triple> propertySchemaTriples = GetDtdlTriplesForRange(relationship.Target, propertyNode); dtdlModel.Assert(propertySchemaTriples); // If there are rdfs:labels, use them for DTDL displayName if (oProperty.Label.Any()) { dtdlModel.Assert(GetDtdlDisplayNameTriples(oProperty, propertyNode)); } // If there are rdfs:comments, generate and assert DTDL description triples from them if (oProperty.Comment.Any()) { dtdlModel.Assert(GetDtdlDescriptionTriples(oProperty, propertyNode)); } } // Write JSON-LD to target file. JObject modelAsJsonLd = ToJsonLd(dtdlModel); interfaceDepths.Add(modelAsJsonLd, oClass.Depth()); if (!_mergedOutput) { // Create model output directory based on output path and longest parent path // I.e., we put multi-inheritance classes as far down as possible in their respective hierarchy List<string> parentDirectories = oClass.LongestParentPathToOwlThing(); string modelPath = string.Join("/", parentDirectories); string modelOutputPath = $"{_outputPath}/{modelPath}/"; // If the class has subclasses, place it with them if (oClass.DirectSubClasses.Any()) { modelOutputPath += $"{oClass.GetLocalName()}/"; } Directory.CreateDirectory(modelOutputPath); string outputFileName = modelOutputPath + oClass.GetLocalName() + ".json"; using (StreamWriter file = File.CreateText(outputFileName)) using (JsonTextWriter writer = new JsonTextWriter(file) { Formatting = Formatting.Indented }) { modelAsJsonLd.WriteTo(writer); } } // Clear the working graph for next iteration dtdlModel.Clear(); } // Sort and generate merged output file // TODO remember what I actually did here and document it if (_mergedOutput) { List<KeyValuePair<JObject, int>> interfacesAndDepths = interfaceDepths.ToList(); interfacesAndDepths.Sort((pair1, pair2) => pair1.Value.CompareTo(pair2.Value)); IEnumerable<JObject> interfaces = interfacesAndDepths.Select(pair => pair.Key); JArray interfaceArray = new JArray(interfaces); Directory.CreateDirectory(_outputPath); string outputFileName = _outputPath + "FullBuildingModels.json"; using (StreamWriter file = File.CreateText(outputFileName)) using (JsonTextWriter writer = new JsonTextWriter(file) { Formatting = Formatting.Indented }) { interfaceArray.WriteTo(writer); } } } // TODO: move this into the DotNetRdfExtensions class internal static IEnumerable<INode> GetAxiomAnnnotations(INode subj, IUriNode pred, INode obj, IUriNode annotationProperty) { IUriNode owlAnnotatedSource = _ontologyGraph.CreateUriNode(OWL.annotatedSource); IUriNode owlAnnotatedProperty = _ontologyGraph.CreateUriNode(OWL.annotatedProperty); IUriNode owlAnnotatedTarget = _ontologyGraph.CreateUriNode(OWL.annotatedTarget); IEnumerable<INode> axiomAnnotations = _ontologyGraph.Nodes .Where(node => _ontologyGraph.ContainsTriple(new Triple(node, owlAnnotatedSource, subj))) .Where(node => _ontologyGraph.ContainsTriple(new Triple(node, owlAnnotatedProperty, pred))) .Where(node => _ontologyGraph.ContainsTriple(new Triple(node, owlAnnotatedTarget, obj))); HashSet<INode> retVal = new HashSet<INode>(); foreach (INode axiomAnnotation in axiomAnnotations) { foreach (Triple annotationAssertion in _ontologyGraph.GetTriplesWithSubjectPredicate(axiomAnnotation, annotationProperty)) { retVal.Add(annotationAssertion.Object); } } return retVal; } // TODO: move this into the DotNetRdfExtensions class internal static bool PropertyAssertionIsDeprecated(INode subj, IUriNode pred, INode obj) { IUriNode owlAnnotatedSource = _ontologyGraph.CreateUriNode(OWL.annotatedSource); IUriNode owlAnnotatedProperty = _ontologyGraph.CreateUriNode(OWL.annotatedProperty); IUriNode owlAnnotatedTarget = _ontologyGraph.CreateUriNode(OWL.annotatedTarget); IUriNode owlDeprecated = _ontologyGraph.CreateUriNode(OWL.deprecated); IEnumerable<INode> axiomAnnotations = _ontologyGraph.Nodes .Where(node => _ontologyGraph.ContainsTriple(new Triple(node, owlAnnotatedSource, subj))) .Where(node => _ontologyGraph.ContainsTriple(new Triple(node, owlAnnotatedProperty, pred))) .Where(node => _ontologyGraph.ContainsTriple(new Triple(node, owlAnnotatedTarget, obj))); foreach (INode axiomAnnotation in axiomAnnotations) { foreach (Triple deprecationAssertion in _ontologyGraph.GetTriplesWithSubjectPredicate(axiomAnnotation, owlDeprecated).Where(trip => trip.Object.NodeType == NodeType.Literal)) { IValuedNode deprecationValue = deprecationAssertion.Object.AsValuedNode(); try { if (deprecationValue.AsBoolean()) { return true; } } catch { } } } return false; } /// <summary> /// /// </summary> private static void ValidateInterfaces() { Console.WriteLine(); Console.WriteLine("Validating DTDL Interface declarations: "); DirectoryInfo dinfo = new DirectoryInfo(_outputPath); var files = dinfo.EnumerateFiles($"*.jsonld", SearchOption.AllDirectories); if (files.Count() == 0) { Log.Alert("No matching files found. Exiting."); return; } Dictionary<FileInfo, string> modelDict = new Dictionary<FileInfo, string>(); int count = 0; string lastFile = "<none>"; try { foreach (FileInfo fi in files) { StreamReader r = new StreamReader(fi.FullName); string dtdl = r.ReadToEnd(); r.Close(); modelDict.Add(fi, dtdl); lastFile = fi.FullName; count++; } } catch (Exception e) { Log.Error($"Could not read files. \nLast file read: {lastFile}\nError: \n{e.Message}"); return; } Log.Ok($"Read {count} files from specified directory"); int errJson = 0; foreach (FileInfo fi in modelDict.Keys) { modelDict.TryGetValue(fi, out string dtdl); try { JsonDocument.Parse(dtdl); } catch (Exception e) { Log.Error($"Invalid json found in file {fi.FullName}.\nJson parser error \n{e.Message}"); errJson++; } } if (errJson > 0) { Log.Error($"\nFound {errJson} Json parsing errors"); return; } Log.Ok($"Validated JSON for all files - now validating DTDL"); List<string> modelList = modelDict.Values.ToList<string>(); ModelParser parser = new ModelParser(); parser.DtmiResolver = delegate (IReadOnlyCollection<Dtmi> dtmis) { Log.Error($"*** Error parsing models. Missing:"); foreach (Dtmi d in dtmis) { Log.Error($" {d}"); } return null; }; try { IReadOnlyDictionary<Dtmi, DTEntityInfo> om = parser.ParseAsync(modelList).GetAwaiter().GetResult(); Log.Out(""); Log.Ok($"**********************************************"); Log.Ok($"** Validated all files - Your DTDL is valid **"); Log.Ok($"**********************************************"); Log.Out($"Found a total of {om.Keys.Count()} entities"); } catch (ParsingException pe) { Log.Error($"*** Error parsing models"); int derrcount = 1; foreach (ParsingError err in pe.Errors) { Log.Error($"Error {derrcount}:"); Log.Error($"{err.Message}"); Log.Error($"Primary ID: {err.PrimaryID}"); Log.Error($"Secondary ID: {err.SecondaryID}"); Log.Error($"Property: {err.Property}\n"); derrcount++; } return; } catch (ResolutionException rex) { Log.Error("Could not resolve required references"); } } /// <summary> /// Generates triples representing a DTDL schema for an OWL (data) property range. /// </summary> /// <param name="owlPropertyRange">The range to translate (typically an XSD datatype or custom datatype)</param> /// <param name="dtdlPropertyNode">The node onto which the generated triples will be grafted</param> /// <returns>Set of triples representing the schema</returns> private static HashSet<Triple> GetDtdlTriplesForRange(OntologyClass owlPropertyRange, INode dtdlPropertyNode) { // TODO: ensure that owlPropertyRange is named! IGraph dtdlModel = dtdlPropertyNode.Graph; IUriNode dtdl_schema = dtdlModel.CreateUriNode(DTDL.schema); IUriNode rdfType = dtdlModel.CreateUriNode(UriFactory.Create(RdfSpecsHelper.RdfType)); IUriNode dtdl_Enum = dtdlModel.CreateUriNode(DTDL.Enum); IUriNode dtdl_valueSchema = dtdlModel.CreateUriNode(DTDL.valueSchema); IUriNode dtdl_enumValues = dtdlModel.CreateUriNode(DTDL.enumValues); IUriNode dtdl_name = dtdlModel.CreateUriNode(VocabularyHelper.DTDL.name); IUriNode dtdl_displayName = dtdlModel.CreateUriNode(VocabularyHelper.DTDL.displayName); IUriNode dtdl_enumValue = dtdlModel.CreateUriNode(DTDL.enumValue); IUriNode dtdl_comment = dtdlModel.CreateUriNode(DTDL.comment); IUriNode dtdl_string = dtdlModel.CreateUriNode(DTDL._string); IUriNode dtdl_unit = dtdlModel.CreateUriNode(DTDL.unit); HashSet<Triple> returnedTriples = new HashSet<Triple>(); // First check for the simple XSD datatypes if (owlPropertyRange.IsXsdDatatype()) { Uri schemaUri = GetXsDatatypeAsDtdlEquivalent(owlPropertyRange); IUriNode schemaNode = dtdlModel.CreateUriNode(schemaUri); returnedTriples.Add(new Triple(dtdlPropertyNode, dtdl_schema, schemaNode)); return returnedTriples; } // Then check for supported custom-defined datatypes if (owlPropertyRange.IsDatatype() && !owlPropertyRange.IsBuiltIn()) { // This is an enumeration of allowed values if (owlPropertyRange.IsEnumerationDatatype()) { IBlankNode enumNode = dtdlModel.CreateBlankNode(); returnedTriples.Add(new Triple(enumNode, rdfType, dtdl_Enum)); returnedTriples.Add(new Triple(dtdlPropertyNode, dtdl_schema, enumNode)); returnedTriples.Add(new Triple(enumNode, dtdl_valueSchema, dtdl_string)); IEnumerable<ILiteralNode> enumOptions = owlPropertyRange.AsEnumeration().LiteralNodes(); foreach (ILiteralNode option in enumOptions) { IBlankNode enumOption = dtdlModel.CreateBlankNode(); returnedTriples.Add(new Triple(enumOption, dtdl_name, dtdlModel.CreateLiteralNode(option.Value))); returnedTriples.Add(new Triple(enumOption, dtdl_enumValue, dtdlModel.CreateLiteralNode(option.Value))); returnedTriples.Add(new Triple(enumNode, dtdl_enumValues, enumOption)); } return returnedTriples; } // This is a wrapper around a XSD standard datatype if (owlPropertyRange.IsSimpleXsdWrapper()) { Uri schemaUri = GetXsDatatypeAsDtdlEquivalent(owlPropertyRange.EquivalentClasses.First()); IUriNode schemaNode = dtdlModel.CreateUriNode(schemaUri); returnedTriples.Add(new Triple(dtdlPropertyNode, dtdl_schema, schemaNode)); // If the wrapper is a punned QUDT unit individual, assign semantic type and unit if (owlPropertyRange.IsQudtUnit() && semanticTypesMap.ContainsKey(owlPropertyRange.GetUri())) { Uri qudtUnit = owlPropertyRange.GetUri(); IUriNode hasQuantityKind = dtdlModel.CreateUriNode(QUDT.hasQuantityKind); IEnumerable<IUriNode> quantityKinds = owlPropertyRange.GetNodesViaPredicate(hasQuantityKind).UriNodes(); if (quantityKinds.Count() == 1 && semanticTypesMap.ContainsKey(quantityKinds.First().Uri)) { Uri qudtQuantityKind = quantityKinds.First().Uri; IUriNode dtdlSemanticType = dtdlModel.CreateUriNode(semanticTypesMap[qudtQuantityKind]); IUriNode dtdlUnit = dtdlModel.CreateUriNode(semanticTypesMap[qudtUnit]); returnedTriples.Add(new Triple(dtdlPropertyNode, rdfType, dtdlSemanticType)); returnedTriples.Add(new Triple(dtdlPropertyNode, dtdl_unit, dtdlUnit)); } } // Assert comment from XSD datatype on parent DTDL property (prioritizing non-tagged, then english, then anything else) if (owlPropertyRange.Comment.Any()) { IEnumerable<ILiteralNode> englishOrNontaggedComments = owlPropertyRange.Comment .Where(node => node.Language == string.Empty || node.Language.StartsWith("en")) .OrderBy(node => node.Language); ILiteralNode comment; if (englishOrNontaggedComments.Any()) { comment = englishOrNontaggedComments.First(); } else { comment = owlPropertyRange.Comment.First(); } ILiteralNode dtdlCommentNode = dtdlModel.CreateLiteralNode(string.Concat(comment.Value.Take(512))); Triple dtdlCommentTriple = new Triple(dtdlPropertyNode, dtdl_comment, dtdlCommentNode); returnedTriples.Add(dtdlCommentTriple); } return returnedTriples; } } // No supported schemas found; fall back to simple string schema IUriNode stringSchemaNode = dtdlModel.CreateUriNode(DTDL._string); returnedTriples.Add(new Triple(dtdlPropertyNode, dtdl_schema, stringSchemaNode)); return returnedTriples; } /// <summary> /// Translate an XSD datatype into a DTDL URI /// </summary> /// <param name="xsdDatatype">XSD datatype to translate</param> /// <returns>DTDL-equivalent URI</returns> private static Uri GetXsDatatypeAsDtdlEquivalent(OntologyClass xsdDatatype) { Dictionary<string, Uri> xsdDtdlPrimitiveTypesMappings = new Dictionary<string, Uri> { {"boolean", DTDL._boolean }, {"byte", DTDL._integer }, {"date", DTDL._date }, {"dateTime", DTDL._dateTime }, {"duration", DTDL._duration }, {"dateTimeStamp", DTDL._dateTime }, {"double", DTDL._double }, {"float", DTDL._float }, {"int", DTDL._integer }, {"integer", DTDL._integer }, {"long", DTDL._long }, {"string",DTDL._string } }; if (xsdDtdlPrimitiveTypesMappings.ContainsKey(xsdDatatype.GetLocalName())) { return xsdDtdlPrimitiveTypesMappings[xsdDatatype.GetLocalName()]; } // Fall-back option return DTDL._string; } /// <summary> /// Checks whether a certain property declaration on a superclass is also defined on any of its subclasses. /// This is necessary since DTDL does not allow sub-interfaces to extend properties from their super-interfaces. /// TODO This is _horribly_ inefficient and is run all the time. Refactor! /// </summary> /// <param name="oClass">Superclass that holds the property being checked</param> /// <param name="checkedForProperty">The property to check for</param> /// <returns>True iff this property is not defined on any subclass</returns> private static bool PropertyIsDefinedOnChildClass(OntologyClass oClass, OntologyProperty checkedForProperty) { foreach (OntologyClass subClass in oClass.SubClasses) { if (subClass.GetRelationships() .Select(relationship => relationship.Property) .Any(property => property.GetUri().AbsoluteUri.Equals(checkedForProperty.GetUri().AbsoluteUri))) { return true; } } return false; } /// <summary> /// Validate that a given name is compliant with the DTDL spec. /// </summary> /// <param name="inputName"></param> /// <returns></returns> private static bool IsCompliantDtdlName(string inputName) { if (inputName.Length > 64) { return false; } Regex regex = new Regex("^[a-zA-Z](?:[a-zA-Z0-9_]*[a-zA-Z0-9])?$"); Match match = regex.Match(inputName); if (match.Success) { return true; } return false; } /// <summary> /// Translates rdfs:label definitions in multiple languages to DTDL display names, returning the triples representing these /// displayNames that need to be grafted onto the model. /// </summary> /// <param name="resource">The resource for which to check for labels</param> /// <param name="subjectNode">The target DTDL node onto which the displayNames will be added</param> /// <returns>Triples representing the display names</returns> private static IEnumerable<Triple> GetDtdlDisplayNameTriples(OntologyResource resource, INode subjectNode) { IGraph dtdlModel = subjectNode.Graph; IEnumerable<ILiteralNode> labels = resource.Label; IUriNode dtdl_displayName = dtdlModel.CreateUriNode(DTDL.displayName); IEnumerable<ILiteralNode> nonLocalizedLabels = labels.Where(node => node.Language == string.Empty); if (nonLocalizedLabels.Any()) { ILiteralNode labelNode = dtdlModel.CreateLiteralNode(string.Concat(nonLocalizedLabels.First().Value.Take(64)), "en"); Triple retVal = new Triple(subjectNode, dtdl_displayName, labelNode); return new List<Triple> { retVal }; } else { List<Triple> triples = new List<Triple>(); foreach (ILiteralNode label in labels) { ILiteralNode labelNode = dtdlModel.CreateLiteralNode(string.Concat(label.Value.Take(64)), label.Language); triples.Add(new Triple(subjectNode, dtdl_displayName, labelNode)); } return triples; } } /// <summary> /// Translates rdfs:comment definitions in multiple languages to DTDL descriptions, returning the triples representing these /// descriptions that need to be grafted onto the model. /// </summary> /// <param name="resource">The resource for which to check for comments</param> /// <param name="subjectNode">The target DTDL node onto which the descriptions will be added</param> /// <returns>Triples representing the descriptions</returns> private static IEnumerable<Triple> GetDtdlDescriptionTriples(OntologyResource resource, INode subjectNode) { IGraph dtdlModel = subjectNode.Graph; IEnumerable<ILiteralNode> comments = resource.Comment; IUriNode dtdl_description = dtdlModel.CreateUriNode(DTDL.description); string inverseDescription = ""; if (resource is OntologyProperty) { OntologyProperty resourceAsProperty = (OntologyProperty)resource; IEnumerable<OntologyProperty> inverses = resourceAsProperty.InverseProperties; if (inverses.Any()) { inverseDescription += $" Inverse of: {string.Join(", ", inverses.Select(property => property.GetLocalName()))}"; } } int lengthOfInverseDescription = inverseDescription.Length; int lengthOfCommentToKeep = 512 - lengthOfInverseDescription; IEnumerable<ILiteralNode> nonLocalizedComments = comments.Where(node => node.Language == string.Empty); if (nonLocalizedComments.Any()) { ILiteralNode commentNode = dtdlModel.CreateLiteralNode(string.Concat(nonLocalizedComments.First().Value.Take(lengthOfCommentToKeep)) + inverseDescription, "en"); Triple retVal = new Triple(subjectNode, dtdl_description, commentNode); return new List<Triple> { retVal }; } else { List<Triple> triples = new List<Triple>(); foreach (ILiteralNode comment in comments) { ILiteralNode commentNode = dtdlModel.CreateLiteralNode(string.Concat(comment.Value.Take(lengthOfCommentToKeep)) + inverseDescription, comment.Language); triples.Add(new Triple(subjectNode, dtdl_description, commentNode)); } return triples; } } /// <summary> /// Generate Digital Twin Model Identifiers; these will be based on reverse dns + path. /// </summary> /// <param name="resource">Resource to generate DTMI for</param> /// <returns>DTMI</returns> private static string GetDTMI(OntologyResource resource) { if (!resource.IsNamed()) { throw new RdfException($"Could not generate DTMI for OntologyResource '{resource}'; resource is anonymous."); } // Get the resource namespace for DTMI minting (see below) Uri resourceNamespace = resource.GetNamespace(); char[] uriTrimChars = { '#', '/' }; // Ensure that the resource is in the namespace mapper. Why do we do this again? if (!namespacePrefixes.ContainsKey(resourceNamespace)) { string namespaceShortName = resourceNamespace.AbsoluteUri.Trim(uriTrimChars).Split('/').Last(); namespacePrefixes[resourceNamespace] = namespaceShortName; } // Combine (reversed) host and path component arrays to create namespace components array string[] hostComponents = resourceNamespace.Host.Split('.'); Array.Reverse(hostComponents); string[] pathComponents = resourceNamespace.AbsolutePath.Trim(uriTrimChars).Split('/'); string[] namespaceComponents = hostComponents.Concat(pathComponents).ToArray(); // The ontologyName is the last component in the namespace array string ontologyName = namespaceComponents.Last(); // If an ontology source has been set at CLI option, use it; otherwise generate from the namespace // components array (omitting the previously extracted ontologyName component) string ontologySource; if (_ontologySource != null) { ontologySource = _ontologySource; } else { string[] ontologySourceComponents = namespaceComponents.Take(namespaceComponents.Count() - 1).ToArray(); ontologySource = string.Join(':', ontologySourceComponents); } // Put together the pieces string dtmi = $"{ontologySource}:{ontologyName}:{resource.GetLocalName()}"; // Run the candidate DTMI through validation per the spec, removing non-conforming chars string[] pathSegments = dtmi.Split(':'); for (int i = 0; i < pathSegments.Length; i++) { string pathSegment = pathSegments[i]; pathSegment = new string((from c in pathSegment where char.IsLetterOrDigit(c) || c.Equals('_') select c ).ToArray()); pathSegment = pathSegment.TrimEnd('_'); pathSegment = pathSegment.TrimStart(new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}); pathSegments[i] = pathSegment; } dtmi = string.Join(':', pathSegments); string dtmiVersion = "1"; if (resource.HasOwlVersionInfo()) { dtmiVersion = resource.GetOwlVersionInfo();; } // Add prefix and suffix return $"dtmi:digitaltwins:{dtmi};{dtmiVersion}"; } /// <summary> /// Do JSON-LD framing and compacting of a model (i.e., a DTDL Interface) using the DTDL context file. /// </summary> /// <param name="dtdlModel">DTDL model to frame/compact, as DotNetRDF graph.</param> /// <returns>JSON-LD representation of input Interface</returns> private static JObject ToJsonLd(Graph dtdlModel) { JArray initialJsonLd; using (TripleStore entitiesStore = new TripleStore()) { entitiesStore.Add(dtdlModel); JsonLdWriterOptions writerOptions = new JsonLdWriterOptions(); writerOptions.UseNativeTypes = true; JsonLdWriter jsonLdWriter = new JsonLdWriter(writerOptions); initialJsonLd = jsonLdWriter.SerializeStore(entitiesStore); } // Configure and run JSON-LD framing and compacting JsonLdProcessorOptions options = new JsonLdProcessorOptions(); options.UseNativeTypes = true; options.Base = new Uri("https://example.org"); // Throwaway base, not actually used JObject frame = new JObject( new JProperty("@type", DTDL.Interface.AbsoluteUri) ); JObject context; using (StreamReader file = File.OpenText(@"DTDL.v2.context.json")) using (JsonTextReader reader = new JsonTextReader(file)) { context = (JObject)JToken.ReadFrom(reader); } JObject framedJson = JsonLdProcessor.Frame(initialJsonLd, frame, options); JObject compactedJson = JsonLdProcessor.Compact(framedJson, context, options); compactedJson["@context"] = new JValue(DTDL.dtdlContext); return compactedJson; } /// <summary> /// Loads imported ontologies transitively. Each imported ontology is added /// to the static set <c>importedOntologies</c>. /// </summary> /// <param name="importedOntology">The ontology to import.</param> private static void LoadImport(Ontology importedOntology) { // We only deal with named ontologies if (importedOntology.IsNamed()) { // Parse and load ontology from the stated import URI Uri importUri = importedOntology.GetUri(); // Only proceed if we have not seen this fetched URI before, otherwise we risk // unecessary fetches and computation, and possibly import loops. if (!importedOntologyUris.Contains(importUri)) { importedOntologyUris.Add(importUri); //Uri importedOntologyUri = ((IUriNode)importedOntology.Resource).Uri; OntologyGraph fetchedOntologyGraph = new OntologyGraph(); try { Console.WriteLine($"Loading {importUri}."); UriLoader.Load(fetchedOntologyGraph, importUri); } catch (RdfParseException e) { Console.Write(e.Message); Console.Write(e.StackTrace); } // Set up a new ontology metadata object from the retrieved ontology graph. // This is needed since this ontology's self-defined IRI or version IRI often // differs from the IRI through which it was imported (i.e., importedOntology in // this method's signature), due to .htaccess redirects, version URIs, etc. Ontology importedOntologyFromFetchedGraph = fetchedOntologyGraph.GetOntology(); // Only proceed if the retrieved ontology has an IRI if (importedOntologyFromFetchedGraph.IsNamed()) { // Add the fetched ontology to the namespace prefix index // (tacking on _1, _2, etc. to the shortname if it exists since before, // since we need all prefix names to be unique). string importedOntologyShortname = importedOntologyFromFetchedGraph.GetShortName(); int i = 1; while (namespacePrefixes.ContainsValue(importedOntologyShortname)) { importedOntologyShortname = importedOntologyShortname.Split('_')[0] + "_" + i; i++; } namespacePrefixes.Add(importedOntologyFromFetchedGraph.GetUri(), importedOntologyShortname); // Merge the fetch graph with the joint ontology graph the tool operates on _ontologyGraph.Merge(fetchedOntologyGraph); // Traverse the imported ontology's import hierarchy transitively foreach (Ontology subImport in importedOntologyFromFetchedGraph.Imports) { LoadImport(subImport); } } // Dispose graph before returning fetchedOntologyGraph.Dispose(); } } } } }