Skip to content

Commit

Permalink
address-space: detect nodeset loading ordering
Browse files Browse the repository at this point in the history
  • Loading branch information
erossignon authored and sterfive committed Nov 3, 2023
1 parent 2840c7c commit d953163
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 178 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,173 @@
import { checkDebugFlag, make_debugLog, make_errorLog } from "node-opcua-debug";
import { CallbackT } from "node-opcua-status-code";
import { IAddressSpace } from "node-opcua-address-space-base";

import { IAddressSpace, RequiredModel } from "node-opcua-address-space-base";
import { ReaderStateParserLike, Xml2Json } from "node-opcua-xml2json";
import { minDate } from "node-opcua-date-time";
import { adjustNamespaceArray } from "../../src/nodeset_tools/adjust_namespace_array";
import { NodeSetLoaderOptions } from "../interfaces/nodeset_loader_options";
import { NamespacePrivate } from "../../src/namespace_private";
import { NodeSetLoader } from "./load_nodeset2";

const doDebug = checkDebugFlag(__filename);
const debugLog = make_debugLog(__filename);
const errorLog = make_errorLog(__filename);

interface Model extends RequiredModel {
requiredModel: RequiredModel[];
}
interface NodesetInfo {
namespaceUris: string[];
models: Model[];
}

async function parseDependencies(xmlData: string): Promise<NodesetInfo> {
const namespaceUris: string[] = [];

const models: Model[] = [];
let currentModel: Model | undefined = undefined;
const state0: ReaderStateParserLike = {
parser: {
UANodeSet: {
parser: {
NamespaceUris: {
parser: {
Uri: {
finish() {
namespaceUris.push(this.text);
}
}
}
},
Models: {
parser: {
Model: {
init(elementName: string, attrs: any) {
const modelUri = attrs.ModelUri;
const version = attrs.Version;
const publicationDate = new Date(Date.parse(attrs.PublicationDate));
currentModel = {
modelUri,
version,
publicationDate,
requiredModel: []
};
doDebug && console.log(`currentModel = ${JSON.stringify(currentModel)}`);
models.push(currentModel);
},
parser: {
RequiredModel: {
init(elementName: string, attrs: any) {
const modelUri = attrs.ModelUri;
const version = attrs.Version;
const publicationDate = new Date(Date.parse(attrs.PublicationDate));

if (!currentModel) {
throw new Error("Internal Error");
}
currentModel.requiredModel.push({
modelUri,
version,
publicationDate
});
}
}
}
}
}
}
}
}
}
};
const parser = new Xml2Json(state0);
parser.parseStringSync(xmlData);
if (models.length === 0 && namespaceUris.length >= 1) {
models.push({
modelUri: namespaceUris[0],
version: "1",
publicationDate: minDate,
requiredModel: []
});
}
return { models, namespaceUris: namespaceUris };
}
interface NodesetDesc {
index: number;
xmlData: string;
namespaceModel: NodesetInfo;
}
/**
* Detect order of namespace loading
*/
export async function preLoad(xmlFiles: string[], xmlLoader: (nodeset2xmlUri: string) => Promise<string>): Promise<NodesetDesc[]> {
// a nodeset2 file may define multiple namespaces
const namespaceDesc: NodesetDesc[] = [];
for (let index = 0; index < xmlFiles.length; index++) {
doDebug && console.log("---------------------------------------------", xmlFiles[index]);
const xmlData = await xmlLoader(xmlFiles[index]);

const indexStart = xmlData.match(/<UANodeSet/m)?.index;
const i1 = (xmlData.match(/<\/Models>/m)?.index || 0) + "</Models>".length;
const i2 = (xmlData.match(/<\/NamespaceUris>/m)?.index || 0) + "</NamespaceUris>".length;

const indexEnd = Math.max(i1, i2);
if (indexStart === undefined || indexEnd === undefined) {
throw new Error("Internal Error");
}
const xmlData2 = xmlData.substring(indexStart, indexEnd);
doDebug &&
console.log(
xmlData2
.split("\n")
.splice(0, 46)
.map((x, i) => `${i + 0} ${x}`)
.join("\n")
);
const namespaceModel = await parseDependencies(xmlData2);
namespaceDesc.push({ xmlData, namespaceModel, index });
}
return namespaceDesc;
}
export function findOrder(nodesetDescs: NodesetDesc[]): number[] {
// compute the order of loading of the namespaces
const order: number[] = [];
const visited: Set<string> = new Set<string>();

const findNodesetIndex = (namespaceUri: string) => {
const index = nodesetDescs.findIndex((x) => x.namespaceModel.models.findIndex((e) => e.modelUri === namespaceUri) !== -1);
return index;
};
const visit = (model: Model) => {
const key = model.modelUri;
if (visited.has(key)) {
return;
}
visited.add(key);
for (const requiredModel of model.requiredModel) {
const requiredModelIndex = findNodesetIndex(requiredModel.modelUri);
if (requiredModelIndex === -1) {
throw new Error("Cannot find namespace for " + requiredModel.modelUri);
}
const nd = nodesetDescs[requiredModelIndex];
for (const n of nd.namespaceModel.models) {
visit(n);
}
}
const nodesetIndex = findNodesetIndex(model.modelUri);
const alreadyIn = order.findIndex((x) => x === nodesetIndex) !== -1;
if (!alreadyIn) order.push(nodesetIndex);
};
const visit2 = (nodesetDesc: NodesetDesc) => {
for (const model of nodesetDesc.namespaceModel.models.values()) {
visit(model);
}
};
for (let index = 0; index < nodesetDescs.length; index++) {
const nodesetDesc = nodesetDescs[index];
visit2(nodesetDesc);
}
return order;
}
/**
* @param addressSpace the addressSpace to populate
* @xmlFiles: a lis of xml files
Expand All @@ -26,15 +184,26 @@ export async function generateAddressSpaceRaw(
if (!Array.isArray(xmlFiles)) {
xmlFiles = [xmlFiles];
}
for (let index = 0; index < xmlFiles.length; index++) {
const xmlData = await xmlLoader(xmlFiles[index]);

const nodesetDesc = await preLoad(xmlFiles, xmlLoader);
const order = findOrder(nodesetDesc);
for (let index = 0; index < order.length; index++) {
const nodesetIndex = order[index];
const nodeset = nodesetDesc[nodesetIndex];
debugLog(" loading ", nodesetIndex, nodeset.xmlData.length);
for (const model of nodeset.namespaceModel.models) {
const ns = addressSpace.registerNamespace(model.modelUri) as NamespacePrivate;
ns.setRequiredModels(model.requiredModel);
}

try {
await nodesetLoader.addNodeSetAsync(xmlData);
await nodesetLoader.addNodeSetAsync(nodeset.xmlData);
} catch (err) {
errorLog("generateAddressSpace: Loading xml file ", xmlFiles[index], " failed with error ", (err as Error).message);
throw err;
}
}

await nodesetLoader.terminateAsync();
adjustNamespaceArray(addressSpace);
// however process them in series
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,13 +306,7 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
// Model must not be already registered
const existingNamespace = addressSpace1.getNamespace(model.modelUri);
if (existingNamespace) {
// special treatment for namespace 0
// istanbul ignore else
if (model.modelUri === "http://opcfoundation.org/UA/") {
namespace = existingNamespace;
} else {
throw new Error(" namespace already registered " + model.modelUri);
}
namespace = existingNamespace;
} else {
namespace = addressSpace1.registerNamespace(model.modelUri);
namespace.setRequiredModels(model.requiredModels);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ describe("testing address space namespace", () => {
});
});

describe("testing address space namespace loading", function (this: any) {
it("should process namespaces and translate namespace index when loading node set xml files", async () => {
describe("testing address space namespace loading", function (this: any) {
it("PIO1 should process namespaces and translate namespace index when loading node set xml files", async () => {
const addressSpace = AddressSpace.create();
const xml_files = [
path.join(__dirname, "../nodesets/mini.Nodeset2.xml"),
Expand Down
4 changes: 4 additions & 0 deletions packages/node-opcua-address-space/test/test_issue_1132.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ describe("#1132 Variable ExtensionObject containing NodeId in nodeset2.xml", ()
});
it("should load a extension object containing a NodeId field", ()=>{
const ns = addressSpace.getNamespaceIndex("http://mynamespace");
if (ns === -1) {
throw new Error("Cannot find namespace");
}

const v = addressSpace.findNode(`ns=${ns};i=1272`) as UAVariable;
console.log(v.readValue().value.value.toJSON() );
v.readValue().value.value.toJSON().should.eql({
Expand Down
4 changes: 2 additions & 2 deletions packages/node-opcua-address-space/test/test_load_nodeset2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ describe("testing NodeSet XML file loading", function (this: any) {
</UADataType>
*/
const xml_file1 = path.join(__dirname, "../test_helpers/test_fixtures/dataType_with_isOptionSet.xml");
const xml_files = [xml_file1];
const xml_files = [nodesets.standard, xml_file1];
await generateAddressSpace(addressSpace, xml_files);

const dataType = addressSpace.findNode("i=95")! as UADataType;
Expand Down Expand Up @@ -552,7 +552,7 @@ describe("testing NodeSet XML file loading", function (this: any) {
communicationLink.field1.should.eql(1);
communicationLink.field2.should.eql(2);
communicationLink.field3.dataType.should.eql(DataType.Int32);

const object = addressSpace.constructExtensionObject(connectionEndpointConfigurationDataType, {
id: "00000000-0000-0000-0000-000000000000",
communicationLinks: [communicationLink]
Expand Down
86 changes: 86 additions & 0 deletions packages/node-opcua-address-space/test/test_nodeset_ordering.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import path from "path";
// import fs from "fs";
import should from "should";
import { nodesets } from "node-opcua-nodesets";
import { getFixture } from "node-opcua-test-fixtures";
import { AddressSpace, preLoad, findOrder, generateAddressSpaceRaw } from "..";
import { readNodeSet2XmlFile } from "../nodeJS";

// const fixtureFolder = path.join(__dirname, "../../test_fixtures");
const doDebug = false;
describe("Ordering NodeSet2 files", () => {
it("NSO-0 should order nodeset files", async () => {
const xmlFiles = [nodesets.standard, nodesets.adi, nodesets.di];
const nodesetDescArray = await preLoad(xmlFiles, readNodeSet2XmlFile);

nodesetDescArray.forEach((x) => (x.xmlData = ""));
doDebug && console.log(JSON.stringify(nodesetDescArray, null, " "));

const order = findOrder(nodesetDescArray);
order.should.eql([0, 2, 1]);
});

it("NSO-1 should order nodeset files 2", async () => {
const xmlFiles = [nodesets.standard, nodesets.adi, nodesets.di];
const addressSpace = AddressSpace.create();
await generateAddressSpaceRaw(addressSpace, xmlFiles, readNodeSet2XmlFile, {});

addressSpace.dispose();
// await addressSpace.shutdown();
});
it("NSO-2 should order nodeset containing multiple namespace", async () => {
const xmlFiles = [path.join(__dirname, "../nodesets/mini.Nodeset2.xml"), getFixture("fixture_custom_nodeset.xml")];

const addressSpace = AddressSpace.create();
await generateAddressSpaceRaw(addressSpace, xmlFiles, readNodeSet2XmlFile, {});
addressSpace.dispose();
});
it("NSO-3 should load ill-formed nodeset containing multiple namespace", async () => {
const xmlFiles = [
nodesets.standard,
nodesets.di,
path.join(__dirname, "../test_helpers/test_fixtures/issue_1132_variable_with_nodeid_value.xml")
];
const addressSpace = AddressSpace.create();
await generateAddressSpaceRaw(addressSpace, xmlFiles, readNodeSet2XmlFile, {});

const diNamespace = addressSpace.getNamespace("http://opcfoundation.org/UA/DI/");
const variable = diNamespace.findNode("i=5001")!;
variable.browseName.toString().should.eql("1:DeviceSet");

const ns = addressSpace.getNamespaceIndex("http://mynamespace");
if (ns === -1) {
throw new Error("Cannot find namespace");
}
addressSpace.dispose();
});
it("NSO-4 should load ill-formed nodeset containing multiple namespace", async () => {
const xmlFile = path.join(__dirname, "../test_helpers/test_fixtures/dataType_with_isOptionSet.xml");

const xmlFiles = [nodesets.standard, xmlFile];

const addressSpace = AddressSpace.create();
await generateAddressSpaceRaw(addressSpace, xmlFiles, readNodeSet2XmlFile, {});

addressSpace.dispose();
});
it("NSO-5 should raise an error if a namespace is missing", async () => {
const xmlFiles = [
nodesets.standard,
/* INTENTIONNALY REMOVED nodesets.di, */
nodesets.adi
];

const addressSpace = AddressSpace.create();
let _err: Error | undefined;
try {
await generateAddressSpaceRaw(addressSpace, xmlFiles, readNodeSet2XmlFile, {});
} catch (err) {
_err = err as Error;
} finally {
addressSpace.dispose();
}
should(_err!).be.instanceOf(Error);
should(_err!.message).match(/Cannot find namespace for http:\/\/opcfoundation.org\/UA\/DI\//);
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<UANodeSet
Version="1.02" LastModified="2013-03-06T05:36:44.0862658Z"
xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">
<UANodeSet Version="1.02" LastModified="2013-03-06T05:36:44.0862658Z"
xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">

<NamespaceUris>
<Uri>http://empty</Uri>
</NamespaceUris>

<Aliases>
<Alias Alias="Boolean">i=1</Alias>
<Alias Alias="NodeId">i=17</Alias>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<UANodeSet Version="1.02" LastModified="2013-03-06T05:36:44.0862658Z"
xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">

<NamespaceUris>
<Uri>http://acme.com/UA/ACME</Uri>
</NamespaceUris>
<Aliases>
<Alias Alias="Boolean">i=1</Alias>
<Alias Alias="SByte">i=2</Alias>
Expand Down

0 comments on commit d953163

Please sign in to comment.