/** * @module node-opcua-address-space */ // produce nodeset xml files import { AddressSpacePrivate } from "../address_space_private"; // tslint:disable:no-var-requires const XMLWriter = require("xml-writer"); import { assert } from "node-opcua-assert"; import { BrowseDirection, LocalizedText, makeNodeClassMask, makeResultMask, NodeClass, makeAccessLevelFlag } from "node-opcua-data-model"; import { QualifiedName } from "node-opcua-data-model"; import { getStructureTypeConstructor, StructuredTypeField, StructuredTypeSchema, hasStructuredType } from "node-opcua-factory"; import { getStructuredTypeSchema } from "node-opcua-factory"; import { NodeId, resolveNodeId } from "node-opcua-nodeid"; import * as utils from "node-opcua-utils"; import { Variant } from "node-opcua-variant"; import { VariantArrayType } from "node-opcua-variant"; import { DataType } from "node-opcua-variant"; import { Namespace, XmlWriter } from "../../source"; import { Int64, minOPCUADate } from "node-opcua-basic-types"; import { BrowseDescription, EnumDefinition, StructureDefinition, StructureField, StructureType } from "node-opcua-types"; import { BaseNode } from "../base_node"; import { UANamespace } from "../namespace"; import { NamespacePrivate } from "../namespace_private"; import { Reference } from "../reference"; import { UADataType } from "../ua_data_type"; import { UAMethod } from "../ua_method"; import { UAObject } from "../ua_object"; import { UAObjectType } from "../ua_object_type"; import { UAReferenceType } from "../ua_reference_type"; import { UAVariable } from "../ua_variable"; import { UAVariableType } from "../ua_variable_type"; import { constructNamespaceDependency } from "./construct_namespace_dependency"; import { ExtensionObject } from "node-opcua-extension-object"; import { make_debugLog } from "node-opcua-debug"; const debugLog = make_debugLog(__filename); function _hash(node: BaseNode | Reference): string { return node.nodeId.toString(); } function _dumpDisplayName(xw: XmlWriter, node: BaseNode): void { if (node.displayName && node.displayName[0]) { xw.startElement("DisplayName").text(node.displayName[0].text!).endElement(); } } function _dumpDescription(xw: XmlWriter, node: BaseNode): void { if (node.description) { let desc = node.description.text; desc = desc || ""; xw.startElement("Description").text(desc).endElement(); } } function translateNodeId(xw: XmlWriter, nodeId: NodeId): NodeId { assert(nodeId instanceof NodeId); const nn = xw.translationTable[nodeId.namespace]; const translatedNode = new NodeId(nodeId.identifierType, nodeId.value, nn); return translatedNode; } function n(xw: XmlWriter, nodeId: NodeId): string { return translateNodeId(xw, nodeId).toString().replace("ns=0;", ""); } function translateBrowseName(xw: XmlWriter, browseName: QualifiedName): QualifiedName { assert(browseName instanceof QualifiedName); const nn = xw.translationTable[browseName.namespaceIndex]; const translatedBrowseName = new QualifiedName({ namespaceIndex: nn, name: browseName.name }); return translatedBrowseName; } function b(xw: XmlWriter, browseName: QualifiedName): string { return translateBrowseName(xw, browseName).toString().replace("ns=0;", ""); } function _dumpReferences(xw: XmlWriter, node: BaseNode) { xw.startElement("References"); const addressSpace = node.addressSpace; const aggregateReferenceType = addressSpace.findReferenceType("Aggregates")!; const hasChildReferenceType = addressSpace.findReferenceType("HasChild")!; const hasSubtypeReferenceType = addressSpace.findReferenceType("HasSubtype")!; const hasTypeDefinitionReferenceType = addressSpace.findReferenceType("HasTypeDefinition")!; const nonHierarchicalReferencesType = addressSpace.findReferenceType("NonHierarchicalReferences")!; const organizesReferencesType = addressSpace.findReferenceType("Organizes")!; const connectsToReferenceType = addressSpace.findReferenceType("ConnectsTo")!; const hasEventSourceReferenceType = addressSpace.findReferenceType("HasEventSource")!; function referenceToKeep(reference: Reference): boolean { const referenceType = reference._referenceType!; // get the direct backward reference to a external namespace if (referenceType.isSupertypeOf(aggregateReferenceType) && !reference.isForward) { if (reference.nodeId.namespace !== node.nodeId.namespace) { // todo: may be check that reference.nodeId.namespace is one of the namespace // on which our namespace is build and not a derived one ! // xx console.log("xxxxxxxxxxxxxx Keeping => ", referenceType.toString(), reference.node?.nodeId.toString()); return true; } } // only keep if (referenceType.isSupertypeOf(aggregateReferenceType) && reference.isForward) { return true; } else if (referenceType.isSupertypeOf(hasSubtypeReferenceType) && !reference.isForward) { return true; } else if (referenceType.isSupertypeOf(hasTypeDefinitionReferenceType) && reference.isForward) { return true; } else if (referenceType.isSupertypeOf(nonHierarchicalReferencesType) && reference.isForward) { return true; } else if (referenceType.isSupertypeOf(organizesReferencesType) && !reference.isForward) { return true; } else if (connectsToReferenceType && referenceType.isSupertypeOf(connectsToReferenceType) && reference.isForward) { return true; } else if (referenceType.isSupertypeOf(hasEventSourceReferenceType) && reference.isForward) { return true; } return false; } const references = node.allReferences().filter(referenceToKeep); for (const reference of references) { if (reference._referenceType!.browseName.toString() === "HasSubtype" && reference.isForward) { continue; } // only output inverse Reference xw.startElement("Reference"); xw.writeAttribute("ReferenceType", b(xw, reference._referenceType!.browseName)); if (!reference.isForward) { xw.writeAttribute("IsForward", reference.isForward ? "true" : "false"); } xw.text(n(xw, reference.nodeId)); xw.endElement(); } xw.endElement(); } function _dumpLocalizedText(xw: XmlWriter, v: LocalizedText) { xw.startElement("Locale"); if (v.locale) { xw.text(v.locale); } xw.endElement(); xw.startElement("Text"); if (v.text) { xw.text(v.text); } xw.endElement(); } function _dumpQualifiedName(xw: XmlWriter, v: QualifiedName) { const t = translateBrowseName(xw, v); if (t.name) { xw.startElement("Name"); xw.text(t.name); xw.endElement(); } if (t.namespaceIndex) { xw.startElement("NamespaceIndex"); xw.text(t.namespaceIndex.toString()); xw.endElement(); } } function _dumpXmlElement(xw: XmlWriter, v: string) { xw.text(v); } /* i=339 1900-01-01T00:00:00Z */ function _dumpExtensionObject(xw: XmlWriter, v: ExtensionObject) { if (!v) { return; } xw.startElement("TypeId"); _dumpNodeId(xw, v.schema.encodingDefaultXml!); xw.endElement(); xw.startElement("Body"); xw.endElement(); } function _dumpNodeId(xw: XmlWriter, v: NodeId) { xw.startElement("Identifier"); xw.text(v.toString()); xw.endElement(); } // tslint:disable:no-console function _dumpVariantValue(xw: XmlWriter, dataType: DataType, value: any) { if (value === undefined || value === null) { return; } switch (dataType) { case DataType.Null: break; case DataType.LocalizedText: xw.startElement(DataType[dataType]); // xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2008/02/Types.xsd"); _dumpLocalizedText(xw, value as LocalizedText); xw.endElement(); break; case DataType.NodeId: xw.startElement(DataType[dataType]); // xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2008/02/Types.xsd"); _dumpNodeId(xw, value as NodeId); xw.endElement(); break; case DataType.DateTime: xw.startElement(DataType[dataType]); // xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2008/02/Types.xsd"); xw.text(value.toISOString()); xw.endElement(); break; case DataType.Int64: case DataType.UInt64: xw.startElement(DataType[dataType]); // xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2008/02/Types.xsd"); xw.text(value[1].toString()); xw.endElement(); break; case DataType.Boolean: case DataType.SByte: case DataType.Byte: case DataType.Float: case DataType.Double: case DataType.Int16: case DataType.Int32: case DataType.UInt16: case DataType.UInt32: case DataType.String: if (value !== undefined && value !== null) { xw.startElement(DataType[dataType]); // xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2008/02/Types.xsd"); xw.text(value.toString()); xw.endElement(); } break; case DataType.ByteString: if (value !== undefined && value !== null) { xw.startElement(DataType[dataType]); // xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2008/02/Types.xsd"); const base64 = value.toString("base64"); xw.text(base64.match(/.{0,80}/g).join("\n")); xw.endElement(); } break; case DataType.Guid: /* 947c29a7-490d-4dc9-adda-1109e3e8fcb7 */ if (value !== undefined && value !== null) { xw.startElement(DataType[dataType]); // xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2008/02/Types.xsd"); xw.startElement("String"); xw.text(value.toString()); xw.endElement(); xw.endElement(); } break; case DataType.ExtensionObject: xw.startElement(DataType[dataType]); _dumpExtensionObject(xw, value as ExtensionObject); xw.endElement(); break; case DataType.QualifiedName: xw.startElement(DataType[dataType]); // xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2008/02/Types.xsd"); _dumpQualifiedName(xw, value as QualifiedName); xw.endElement(); break; case DataType.XmlElement: xw.startElement(DataType[dataType]); // xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2008/02/Types.xsd"); _dumpXmlElement(xw, value as string); xw.endElement(); break; case DataType.StatusCode: default: throw new Error( "_dumpVariantValue!! incomplete dataType=" + dataType + " - v=" + DataType[dataType] + " value = " + value ); } } // tslint:disable:no-console function _dumpVariantInnerValue(xw: XmlWriter, dataType: DataType, value: any) { switch (dataType) { case null: case DataType.Null: break; case DataType.LocalizedText: _dumpLocalizedText(xw, value as LocalizedText); break; case DataType.QualifiedName: _dumpQualifiedName(xw, value as QualifiedName); break; case DataType.NodeId: _dumpNodeId(xw, value as NodeId); break; case DataType.DateTime: xw.text(value.toISOString()); break; case DataType.Int64: case DataType.UInt64: xw.text(value[1].toString()); break; case DataType.Boolean: case DataType.Byte: case DataType.Float: case DataType.Double: case DataType.Int16: case DataType.Int32: case DataType.UInt16: case DataType.UInt32: case DataType.String: xw.text(value.toString()); break; case DataType.ByteString: case DataType.StatusCode: default: throw new Error("_dumpVariantInnerValue incomplete " + value + " " + "DataType=" + dataType + "=" + DataType[dataType]); } } /** * * @param field */ function findBaseDataType(field: StructuredTypeField): DataType { if (field.fieldType === "UAString") { return DataType.String; } const result = (DataType as any)[field.fieldType] as DataType; if (!result) { throw new Error("cannot find baseDataType of " + field.name + "= " + field.fieldType); } return result; } /** * * @param xw * @param schema * @param value * @private */ function _dumpVariantExtensionObjectValue_Body(xw: XmlWriter, schema: StructuredTypeSchema, value: any) { if (value) { xw.startElement(schema.name); if (value) { for (const field of schema.fields) { const v = value[field.name]; if (v !== null && v !== undefined) { xw.startElement(utils.capitalizeFirstLetter(field.name)); try { const baseType = findBaseDataType(field); _dumpVariantInnerValue(xw, baseType, v); } catch (err) { console.log("Error in _dumpVariantExtensionObjectValue_Body !!!", err.message); console.log(schema.name); console.log(field); // throw err; } xw.endElement(); } } } xw.endElement(); } } /* encode object as XML */ function _dumpVariantExtensionObjectValue(xw: XmlWriter, schema: StructuredTypeSchema, value: any) { xw.startElement("ExtensionObject"); { xw.startElement("TypeId"); { // find HasEncoding node const encodingDefaultXml = (getStructureTypeConstructor(schema.name) as any).encodingDefaultXml; if (!encodingDefaultXml) { console.log("?????"); } // xx var encodingDefaultXml = schema.encodingDefaultXml; xw.startElement("Identifier"); xw.text(encodingDefaultXml.toString()); xw.endElement(); } xw.endElement(); xw.startElement("Body"); _dumpVariantExtensionObjectValue_Body(xw, schema, value); xw.endElement(); } xw.endElement(); } function _isDefaultValue(value: Variant): boolean { // detect default value if (value.arrayType === VariantArrayType.Scalar) { switch (value.dataType) { case DataType.ExtensionObject: if (!value.value) { return true; } break; case DataType.DateTime: if (!value.value || value.value.getTime() === minOPCUADate) { return true; } break; case DataType.ByteString: if (!value.value || value.value.length === 0) { return true; } break; case DataType.Boolean: if (!value.value) { return true; } break; case DataType.SByte: case DataType.Byte: case DataType.UInt16: case DataType.UInt32: case DataType.Int16: case DataType.Int32: case DataType.Double: case DataType.Float: if (value.value === 0 || value.value === null) { return true; } break; case DataType.String: if (value.value === null || value.value === "") { return true; } break; case DataType.UInt64: case DataType.UInt64: if (0 === coerceInt64ToInt32(value.value)) { return true; } break; case DataType.LocalizedText: if (!value.value) { return true; } const l = value.value as LocalizedText; if (!l.locale && !l.text) { return true; } break; } return false; } else { if (!value.value || value.value.length === 0) { return true; } return false; } } function _dumpValue(xw: XmlWriter, node: UAVariable | UAVariableType, value: Variant) { const addressSpace = node.addressSpace; // istanbul ignore next if (value === null || value === undefined) { return; } assert(value instanceof Variant); const dataTypeNode = addressSpace.findNode(node.dataType); if (!dataTypeNode) { console.log("Cannot find dataType:", node.dataType); return; } const dataTypeName = dataTypeNode.browseName.name!.toString(); const baseDataTypeName = DataType[value.dataType]; if (baseDataTypeName === "Null") { return; } assert(typeof baseDataTypeName === "string"); // determine if dataTypeName is a ExtensionObject const isExtensionObject = value.dataType === DataType.ExtensionObject; if (_isDefaultValue(value)) { return; } xw.startElement("Value"); if (isExtensionObject) { if (hasStructuredType(dataTypeName)) { const schema = getStructuredTypeSchema(dataTypeName); const encodeXml = _dumpVariantExtensionObjectValue.bind(null, xw, schema); if (value.arrayType === VariantArrayType.Array) { xw.startElement("ListOf" + baseDataTypeName); value.value.forEach(encodeXml); xw.endElement(); } else if (value.arrayType === VariantArrayType.Scalar) { encodeXml(value.value); } else { throw new Error("Unsupported case"); } } } else { const encodeXml = _dumpVariantValue.bind(null, xw, value.dataType); if (value.arrayType === VariantArrayType.Matrix) { console.log("Warning _dumpValue : Matrix not supported yet"); xw.startElement("ListOf" + dataTypeName); xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2008/02/Types.xsd"); value.value.forEach(encodeXml); xw.endElement(); } else if (value.arrayType === VariantArrayType.Array) { xw.startElement("ListOf" + dataTypeName); xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2008/02/Types.xsd"); value.value.forEach(encodeXml); xw.endElement(); } else if (value.arrayType === VariantArrayType.Scalar) { encodeXml(value.value); } else { throw new Error("Unsupported case"); } } xw.endElement(); } function _dumpArrayDimensionsAttribute(xw: XmlWriter, node: UAVariableType | UAVariable) { if (node.arrayDimensions) { if (node.arrayDimensions.length === 1 && node.arrayDimensions[0] === 0) { return; } xw.writeAttribute("ArrayDimensions", node.arrayDimensions.join(",")); } } function visitUANode(node: BaseNode, options: any, forward: boolean) { assert(typeof forward === "boolean"); const addressSpace = node.addressSpace; options.elements = options.elements || []; options.index_el = options.index_el || {}; // visit references function process_reference(reference: Reference) { // only backward or forward references if (reference.isForward !== forward) { return; } if (reference.nodeId.namespace === 0) { return; // skip OPCUA namespace } const k = _hash(reference); if (!options.index_el[k]) { options.index_el[k] = 1; const o = addressSpace.findNode(k)! as BaseNode; if (o) { visitUANode(o, options, forward); } } } node.ownReferences().forEach(process_reference); options.elements.push(node); return node; } function dumpReferencedNodes(xw: XmlWriter, node: BaseNode, forward: boolean) { const addressSpace = node.addressSpace; if (!forward) { { const r = node.findReferencesEx("HasTypeDefinition"); if (r && r.length) { assert(r.length === 1); const typeDefinitionObj = Reference.resolveReferenceNode(addressSpace, r[0])! as BaseNode; if (!typeDefinitionObj) { console.log(node.toString()); console.log("Warning : " + node.browseName.toString() + " unknown typeDefinition, ", r[0].toString()); } else { assert(typeDefinitionObj instanceof BaseNode); if (typeDefinitionObj.nodeId.namespace === node.nodeId.namespace) { // only output node if it is on the same namespace if (!xw.visitedNode[_hash(typeDefinitionObj)]) { typeDefinitionObj.dumpXML(xw); } } } } } // { const r = node.findReferencesEx("HasSubtype", BrowseDirection.Inverse); if (r && r.length) { const subTypeOf = Reference.resolveReferenceNode(addressSpace, r[0])! as BaseNode; assert(r.length === 1); if (subTypeOf.nodeId.namespace === node.nodeId.namespace) { // only output node if it is on the same namespace if (!xw.visitedNode[_hash(subTypeOf)]) { subTypeOf.dumpXML(xw); } } } } } else { const r = node.findReferencesEx("Aggregates", BrowseDirection.Forward); for (const reference of r) { const nodeChild = Reference.resolveReferenceNode(addressSpace, reference) as BaseNode; assert(nodeChild instanceof BaseNode); if (nodeChild.nodeId.namespace === node.nodeId.namespace) { if (!xw.visitedNode[_hash(nodeChild)]) { console.log( node.nodeId.toString(), " dumping child ", nodeChild.browseName.toString(), nodeChild.nodeId.toString() ); nodeChild.dumpXML(xw); } } } } } const currentReadFlag = makeAccessLevelFlag("CurrentRead"); function dumpCommonAttributes(xw: XmlWriter, node: BaseNode) { xw.writeAttribute("NodeId", n(xw, node.nodeId)); xw.writeAttribute("BrowseName", b(xw, node.browseName)); if (node.hasOwnProperty("symbolicName")) { xw.writeAttribute("SymbolicName", (node as any).symbolicName); } if (node.hasOwnProperty("isAbstract")) { if ((node as any).isAbstract) { xw.writeAttribute("IsAbstract", (node as any).isAbstract ? "true" : "false"); } } if (node.hasOwnProperty("accessLevel")) { // CurrentRead is by default if ((node as UAVariable).accessLevel !== currentReadFlag) { xw.writeAttribute("AccessLevel", (node as UAVariable).accessLevel.toString()); } } } function dumpCommonElements(xw: XmlWriter, node: BaseNode) { _dumpDisplayName(xw, node); _dumpDescription(xw, node); _dumpReferences(xw, node); } function coerceInt64ToInt32(int64: Int64): number { if (typeof int64 === "number") { return int64 as number; } if (int64[0] === 4294967295 && int64[1] === 4294967295) { return 0xffffffff; } if (int64[0] !== 0) { debugLog("coerceInt64ToInt32 , loosing high word in conversion"); }; return int64[1]; } function _dumpEnumDefinition(xw: XmlWriter, enumDefinition: EnumDefinition) { enumDefinition.fields = enumDefinition.fields || []; for (const defItem of enumDefinition.fields!) { xw.startElement("Field"); xw.writeAttribute("Name", defItem.name as string); if (!utils.isNullOrUndefined(defItem.value)) { xw.writeAttribute("Value", coerceInt64ToInt32(defItem.value)); } if (defItem.description && defItem.description.text) { xw.startElement("Description"); xw.text(defItem.description.text.toString()); xw.endElement(); } xw.endElement(); } } function _dumpStructureDefinition(xw: XmlWriter, structureDefinition: StructureDefinition) { /* * note: baseDataType and defaultEncodingId are implicit and not stored in the XML file ?? * */ const baseDataType = structureDefinition.baseDataType; const defaultEncodingId = structureDefinition.defaultEncodingId; structureDefinition.fields = structureDefinition.fields || []; for (const defItem /*: StructureField*/ of structureDefinition.fields) { xw.startElement("Field"); xw.writeAttribute("Name", defItem.name!); if (defItem.arrayDimensions) { xw.writeAttribute("ArrayDimensions", defItem.arrayDimensions.map((x) => x.toString()).join(",")); } if (defItem.valueRank !== undefined && defItem.valueRank !== -1) { xw.writeAttribute("ValueRank", defItem.valueRank); } if (defItem.isOptional /* && defItem.isOptional !== false */) { xw.writeAttribute("IsOptional", defItem.isOptional.toString()); } if (defItem.maxStringLength !== undefined && defItem.maxStringLength !== 0) { xw.writeAttribute("MaxStringLength", defItem.maxStringLength); } // todo : SymbolicName ( see AutoId ) if (defItem.dataType) { // todo : namespace translation ! xw.writeAttribute("DataType", n(xw, defItem.dataType)); } if (defItem.description && defItem.description.text) { xw.startElement("Description"); xw.text(defItem.description.text.toString()); xw.endElement(); } xw.endElement(); } } function _dumpUADataTypeDefinition(xw: XmlWriter, node: UADataType) { const $definition = node._getDefinition(); if (!$definition) { return; } if ($definition instanceof EnumDefinition) { xw.startElement("Definition"); xw.writeAttribute("Name", node.browseName.name!); _dumpEnumDefinition(xw, $definition); xw.endElement(); return; } if ($definition instanceof StructureDefinition) { xw.startElement("Definition"); xw.writeAttribute("Name", node.browseName.name!); if ($definition.structureType === StructureType.Union) { xw.writeAttribute("IsUnion", "true"); } /* // todo $definition.baseDataType; // todo $definition.structureType; */ _dumpStructureDefinition(xw, $definition); xw.endElement(); return; } // throw new Error("_dumpUADataTypeDefinition: Should not get here !"); } function dumpUADataType(xw: XmlWriter, node: UADataType) { _markAsVisited(xw, node); xw.startElement("UADataType"); xw.writeAttribute("NodeId", n(xw, node.nodeId)); xw.writeAttribute("BrowseName", b(xw, node.browseName)); if (node.symbolicName !== node.browseName.name) { xw.writeAttribute("SymbolicName", node.symbolicName); } if (node.isAbstract) { xw.writeAttribute("IsAbstract", node.isAbstract ? "true" : "false"); } _dumpDisplayName(xw, node); _dumpReferences(xw, node); _dumpUADataTypeDefinition(xw, node); xw.endElement(); dumpAggregates(xw, node); } UADataType.prototype.dumpXML = function (xw: XmlWriter) { dumpUADataType(xw, this); }; function _markAsVisited(xw: XmlWriter, node: BaseNode) { xw.visitedNode = xw.visitedNode || {}; assert(!xw.visitedNode[_hash(node)]); xw.visitedNode[_hash(node)] = 1; } function dumpUAVariable(xw: XmlWriter, node: UAVariable) { _markAsVisited(xw, node); dumpReferencedNodes(xw, node, false); const addressSpace = node.addressSpace; xw.startElement("UAVariable"); { // attributes dumpCommonAttributes(xw, node); if (node.valueRank !== -1) { // -1 = Scalar xw.writeAttribute("ValueRank", node.valueRank); } _dumpArrayDimensionsAttribute(xw, node); const dataTypeNode = addressSpace.findNode(node.dataType); if (dataTypeNode) { // verify that data Type is in alias // xx const dataTypeName = dataTypeNode.browseName.toString(); const dataTypeName = b(xw, resolveDataTypeName(addressSpace, dataTypeNode.nodeId)); xw.writeAttribute("DataType", dataTypeName); } } { // sub elements dumpCommonElements(xw, node); _dumpValue(xw, node, node.readValue().value); } xw.endElement(); dumpAggregates(xw, node); } UAVariable.prototype.dumpXML = function (xw) { dumpUAVariable(xw, this); }; UAReferenceType.prototype.dumpXML = function (xw) { dumpReferenceType(xw, this); }; function dumpUAVariableType(xw: XmlWriter, node: UAVariableType) { xw.visitedNode = xw.visitedNode || {}; assert(!xw.visitedNode[_hash(node)]); xw.visitedNode[_hash(node)] = 1; dumpReferencedNodes(xw, node, false); const addressSpace = node.addressSpace; xw.startElement("UAVariableType"); { // attributes dumpCommonAttributes(xw, node); if (node.valueRank !== -1) { xw.writeAttribute("ValueRank", node.valueRank); } const dataTypeNode = addressSpace.findNode(node.dataType); if (!dataTypeNode) { // throw new Error(" cannot find datatype " + node.dataType); console.log( " cannot find datatype " + node.dataType + " for node " + node.browseName.toString() + " id =" + node.nodeId.toString() ); } else { const dataTypeName = b(xw, resolveDataTypeName(addressSpace, dataTypeNode.nodeId)); xw.writeAttribute("DataType", dataTypeName); } } { _dumpArrayDimensionsAttribute(xw, node); // sub elements dumpCommonElements(xw, node); _dumpValue(xw, node, node.value); } xw.endElement(); dumpAggregates(xw, node); } UAVariableType.prototype.dumpXML = function (xw) { dumpUAVariableType(xw, this); }; function dumpUAObject(xw: XmlWriter, node: UAObject) { xw.writeComment("Object - " + b(xw, node.browseName) + " {{{{ "); xw.visitedNode = xw.visitedNode || {}; assert(!xw.visitedNode[_hash(node)]); xw.visitedNode[_hash(node)] = 1; // dump SubTypeOf and HasTypeDefinition dumpReferencedNodes(xw, node, false); xw.startElement("UAObject"); dumpCommonAttributes(xw, node); dumpCommonElements(xw, node); xw.endElement(); // dump aggregates nodes ( Properties / components ) dumpAggregates(xw, node); dumpElementInFolder(xw, node); xw.writeComment("Object - " + b(xw, node.browseName) + " }}}} "); } UAObject.prototype.dumpXML = function (xw) { dumpUAObject(xw, this); }; function dumpElementInFolder(xw: XmlWriter, node: BaseNode) { const aggregates = node .getFolderElements() .sort((x: BaseNode, y: BaseNode) => (x.browseName.name!.toString() > y.browseName.name!.toString() ? 1 : -1)); for (const aggregate of aggregates) { // do not export node that do not belong to our namespace if (node.nodeId.namespace !== aggregate.nodeId.namespace) { return; } if (!xw.visitedNode[_hash(aggregate)]) { aggregate.dumpXML(xw); } } } function dumpAggregates(xw: XmlWriter, node: BaseNode) { // Xx xw.writeComment("Aggregates {{ "); const aggregates = node .getAggregates() .sort((x: BaseNode, y: BaseNode) => (x.browseName.name!.toString() > y.browseName.name!.toString() ? 1 : -1)); for (const aggregate of aggregates) { // do not export node that do not belong to our namespace if (node.nodeId.namespace !== aggregate.nodeId.namespace) { return; } if (!xw.visitedNode[_hash(aggregate)]) { aggregate.dumpXML(xw); } } // Xx xw.writeComment("Aggregates }} "); } function dumpUAObjectType(xw: XmlWriter, node: UAObjectType) { assert(node instanceof UAObjectType); xw.writeComment("ObjectType - " + b(xw, node.browseName) + " {{{{ "); _markAsVisited(xw, node); // dump SubtypeOf and HasTypeDefinition dumpReferencedNodes(xw, node, false); xw.startElement("UAObjectType"); dumpCommonAttributes(xw, node); dumpCommonElements(xw, node); xw.endElement(); dumpAggregates(xw, node); xw.writeComment("ObjectType - " + b(xw, node.browseName) + " }}}}"); } UAObjectType.prototype.dumpXML = function (xw) { dumpUAObjectType(xw, this); }; function dumpUAMethod(xw: XmlWriter, node: UAMethod) { _markAsVisited(xw, node); dumpReferencedNodes(xw, node, false); xw.startElement("UAMethod"); dumpCommonAttributes(xw, node); if (node.methodDeclarationId) { xw.writeAttribute("MethodDeclarationId", n(xw, node.methodDeclarationId)); } dumpCommonElements(xw, node); xw.endElement(); dumpAggregates(xw, node); } UAMethod.prototype.dumpXML = function (xw) { dumpUAMethod(xw, this); }; function resolveDataTypeName(addressSpace: AddressSpacePrivate, dataType: string | NodeId): QualifiedName { let dataTypeNode = null; // istanbul ignore next if (typeof dataType === "string") { dataTypeNode = addressSpace.findDataType(dataType); } else { assert(dataType instanceof NodeId); const o = addressSpace.findNode(dataType.toString()); dataTypeNode = o ? o : null; } if (!dataTypeNode) { throw new Error("Cannot find dataTypeName " + dataType); } return dataTypeNode.browseName; } function buildUpAliases(node: BaseNode, xw: XmlWriter, options: any) { const addressSpace = node.addressSpace; options.aliases = options.aliases || {}; options.aliases_visited = options.aliases_visited || {}; const k = _hash(node); // istanbul ignore next if (options.aliases_visited[k]) { return; } options.aliases_visited[k] = 1; // put datatype into aliases list if (node.nodeClass === NodeClass.Variable || node.nodeClass === NodeClass.VariableType) { const nodeV = node as UAVariableType | UAVariable; if (nodeV.dataType && nodeV.dataType.namespace === 0 && nodeV.dataType.value !== 0) { // name const dataTypeName = b(xw, resolveDataTypeName(addressSpace, nodeV.dataType)); if (dataTypeName) { if (!options.aliases[dataTypeName]) { options.aliases[dataTypeName] = n(xw, nodeV.dataType); } } } if (nodeV.dataType && nodeV.dataType.namespace !== 0 && nodeV.dataType.value !== 0) { // name const dataTypeName = b(xw, resolveDataTypeName(addressSpace, nodeV.dataType)); if (dataTypeName) { if (!options.aliases[dataTypeName]) { options.aliases[dataTypeName] = n(xw, nodeV.dataType); } } } } function collectReferenceNameInAlias(reference: Reference) { // reference.referenceType const key = b(xw, reference._referenceType!.browseName); if (!options.aliases.key) { if (reference.referenceType.namespace === 0) { options.aliases[key] = reference.referenceType.toString().replace("ns=0;", ""); } else { options.aliases[key] = n(xw, reference.referenceType); } } } node.allReferences().forEach(collectReferenceNameInAlias); } function writeAliases(xw: XmlWriter, aliases: any) { xw.startElement("Aliases"); if (aliases) { const keys = Object.keys(aliases).sort(); for (const key of keys) { xw.startElement("Alias"); xw.writeAttribute("Alias", key); xw.text(aliases[key].toString().replace(/ns=0;/, "")); xw.endElement(); } } xw.endElement(); } interface ITranslationTable { [key: number]: number; } function constructNamespaceTranslationTable(dependency: Namespace[]): ITranslationTable { const translationTable: ITranslationTable = {}; for (let i = 0; i < dependency.length; i++) { translationTable[dependency[i].index] = i; } return translationTable; } function dumpReferenceType(xw: XmlWriter, referenceType: UAReferenceType) { _markAsVisited(xw, referenceType); xw.startElement("UAReferenceType"); dumpCommonAttributes(xw, referenceType); dumpCommonElements(xw, referenceType); if (referenceType.inverseName /* LocalizedText*/) { xw.startElement("InverseName"); xw.text(referenceType.inverseName!.text || ""); xw.endElement(); } xw.endElement(); } function sortByBrowseName(x: BaseNode, y: BaseNode): number { const x_str = x.browseName.toString(); const y_str = y.browseName.toString(); if (x_str > y_str) { return -1; } else if (x_str < y_str) { return 1; } return 0; } export function dumpXml(node: BaseNode, options: any) { const namespace = node.namespace as NamespacePrivate; // make a first visit so that we determine which node to output and in which order const nodesToVisit: any = {}; const dependency = constructNamespaceDependency(namespace); const translationTable = constructNamespaceTranslationTable(dependency); const xw = new XMLWriter(true); xw.translationTable = translationTable; visitUANode(node, nodesToVisit, false); xw.startDocument({ encoding: "utf-8" }); xw.startElement("UANodeSet"); xw.writeAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema-instance"); xw.writeAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema"); xw.writeAttribute("Version", "1.02"); xw.writeAttribute("LastModified", new Date().toISOString()); xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2011/03/UANodeSet.xsd"); buildUpAliases(node, xw, nodesToVisit); writeAliases(xw, nodesToVisit.aliases); for (const el of nodesToVisit.elements) { el.dumpXML(xw); } xw.endElement(); xw.endDocument(); return xw.toString(); } UANamespace.prototype.toNodeset2XML = function (this: UANamespace) { const dependency = constructNamespaceDependency(this); const translationTable = constructNamespaceTranslationTable(dependency); const xw = new XMLWriter(true); xw.translationTable = translationTable; xw.startDocument({ encoding: "utf-8", version: "1.0" }); xw.startElement("UANodeSet"); xw.writeAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); xw.writeAttribute("xmlns:uax", "http://opcfoundation.org/UA/2008/02/Types.xsd"); xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2011/03/UANodeSet.xsd"); // xx xw.writeAttribute("Version", "1.02"); // xx xw.writeAttribute("LastModified", (new Date()).toISOString()); // ------------- Namespace Uris xw.startElement("NamespaceUris"); // xx const namespaceArray = namespace.addressSpace.getNamespaceArray(); for (const depend of dependency) { if (depend.index === 0) { continue; // ignore namespace 0 } xw.startElement("Uri"); xw.text(depend.namespaceUri); xw.endElement(); } xw.endElement(); // ------------- Namespace Uris xw.startElement("Models"); xw.endElement(); const s: any = {}; for (const node of this.nodeIterator()) { buildUpAliases(node, xw, s); } writeAliases(xw, s.aliases); xw.visitedNode = {}; // -------------- writeReferences xw.writeComment("ReferenceTypes"); const referenceTypes = [...this._referenceTypeIterator()].sort(sortByBrowseName); for (const referenceType of referenceTypes) { dumpReferenceType(xw, referenceType); } // -------------- Dictionaries const addressSpace = this.addressSpace; const opcBinaryTypeSystem = addressSpace.findNode("OPCBinarySchema_TypeSystem") as UAObject; if (opcBinaryTypeSystem) { // let find all DataType dictionary node corresponding to a given namespace // (have DataTypeDictionaryType) const nodeToBrowse = new BrowseDescription({ browseDirection: BrowseDirection.Forward, includeSubtypes: false, nodeClassMask: makeNodeClassMask("Variable"), nodeId: opcBinaryTypeSystem.nodeId, referenceTypeId: resolveNodeId("HasComponent"), resultMask: makeResultMask("ReferenceType | IsForward | BrowseName | NodeClass | TypeDefinition") }); const result = opcBinaryTypeSystem.browseNode(nodeToBrowse).filter((r) => r.nodeId.namespace === this.index); assert(result.length <= 1); if (result.length === 1) { xw.writeComment("DataSystem"); const dataSystemType = addressSpace.findNode(result[0].nodeId)! as UAVariable; dataSystemType.dumpXML(xw); } } // -------------- DataTypes const dataTypes = [...this._dataTypeIterator()].sort(sortByBrowseName); if (dataTypes.length) { xw.writeComment("DataTypes"); // xx xw.writeComment(" "+ objectTypes.map(x=>x.browseName.name.toString()).join(" ")); for (const dataType of dataTypes) { if (!xw.visitedNode[_hash(dataType)]) { dataType.dumpXML(xw); } } } // -------------- ObjectTypes xw.writeComment("ObjectTypes"); const objectTypes = [...this._objectTypeIterator()].sort(sortByBrowseName); // xx xw.writeComment(" "+ objectTypes.map(x=>x.browseName.name.toString()).join(" ")); for (const objectType of objectTypes) { if (!xw.visitedNode[_hash(objectType)]) { objectType.dumpXML(xw); } } // -------------- VariableTypes xw.writeComment("VariableTypes"); const variableTypes = [...this._variableTypeIterator()].sort(sortByBrowseName); // xx xw.writeComment("ObjectTypes "+ variableTypes.map(x=>x.browseName.name.toString()).join(" ")); for (const variableType of variableTypes) { if (!xw.visitedNode[_hash(variableType)]) { variableType.dumpXML(xw); } } // -------------- Any thing else xw.writeComment("Other Nodes"); const nodes = [...this.nodeIterator()].sort(sortByBrowseName); for (const node of nodes) { if (!xw.visitedNode[_hash(node)]) { node.dumpXML(xw); } } xw.endElement(); xw.endDocument(); return xw.toString(); };