/* eslint-disable complexity */ /** * @module node-opcua-address-space */ // produce nodeset xml files import { types } from "util"; import { assert } from "node-opcua-assert"; import { ObjectIds, VariableIds } from "node-opcua-constants"; import { make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug"; import { ExtensionObject } from "node-opcua-extension-object"; import { BrowseDirection, LocalizedText, makeNodeClassMask, makeResultMask, NodeClass, makeAccessLevelFlag, QualifiedName } from "node-opcua-data-model"; import { NodeId, NodeIdType, resolveNodeId } from "node-opcua-nodeid"; import { lowerFirstLetter, isNullOrUndefined } from "node-opcua-utils"; import { Variant, VariantArrayType, DataType } from "node-opcua-variant"; import { IAddressSpace, BaseNode, UADataType, UAMethod, UAObject, UAReference, UAReferenceType, UAVariable, UAVariableType, UAView } from "node-opcua-address-space-base"; import { AttributeIds, Int64, isMinDate, StatusCode } from "node-opcua-basic-types"; import { BrowseDescription, EnumDefinition, StructureDefinition, StructureType } from "node-opcua-types"; import { XmlWriter } from "../../source/xml_writer"; import { ReferenceImpl } from "../reference_impl"; import { BaseNodeImpl, getReferenceType } from "../base_node_impl"; import { UAReferenceTypeImpl } from "../ua_reference_type_impl"; import { UAObjectTypeImpl } from "../ua_object_type_impl"; import { UAVariableImpl } from "../ua_variable_impl"; import { UAObjectImpl } from "../ua_object_impl"; import { NamespaceImpl } from "../namespace_impl"; import { UAMethodImpl } from "../ua_method_impl"; import { UADataTypeImpl } from "../ua_data_type_impl"; import { UAVariableTypeImpl } from "../ua_variable_type_impl"; import { AddressSpace, SessionContext } from "../index_current"; import { UAViewImpl } from "../ua_view_impl"; import { DefinitionMap2 } from "../../source/loader/make_xml_extension_object_parser"; import { makeDefinitionMap } from "../../source/loader/decode_xml_extension_object"; import { _constructNamespaceTranslationTable, constructNamespaceDependency, constructNamespacePriorityTable } from "./construct_namespace_dependency"; import { start } from "repl"; // tslint:disable:no-var-requires const XMLWriter = require("xml-writer"); const debugLog = make_debugLog(__filename); const warningLog = make_warningLog(__filename); const errorLog = make_errorLog(__filename); const doDebug = false; function _hash(node: BaseNode | UAReference): 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: { description?: LocalizedText }): void { if (node.description && node.description.text && node.description.text.length) { 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.get(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.get(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 hasHigherPriorityThan(namespaceIndex1: number, namespaceIndex2: number, priorityTable: number[]) { const order1 = priorityTable[namespaceIndex1]; const order2 = priorityTable[namespaceIndex2]; return order1 > order2; } function _hasHigherPriorityThan(xw: XmlWriter, namespaceIndex1: number, namespaceIndex2: number) { assert(xw.priorityTable, "expecting a priorityTable"); assert(namespaceIndex1 < xw.priorityTable.length); assert(namespaceIndex2 < xw.priorityTable.length); return hasHigherPriorityThan(namespaceIndex1, namespaceIndex2, xw.priorityTable); } 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: UAReference): boolean { const referenceType = (reference as ReferenceImpl)._referenceType!; const targetedNamespaceIndex = reference.nodeId.namespace; if (_hasHigherPriorityThan(xw, targetedNamespaceIndex, node.nodeId.namespace)) { // this reference has nothing to do here ! drop it // because the target namespace is higher in the hierarchy return false; } // get the direct backward reference to a external namespace if (referenceType.isSubtypeOf(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 ! return true; } } if (referenceType.isSubtypeOf(hasSubtypeReferenceType) && reference.isForward) { // return false; } // only keep if (referenceType.isSubtypeOf(aggregateReferenceType) && reference.isForward) { return true; } else if (referenceType.isSubtypeOf(hasSubtypeReferenceType) && !reference.isForward) { return true; } else if (referenceType.isSubtypeOf(hasTypeDefinitionReferenceType) && reference.isForward) { return true; } else if (referenceType.isSubtypeOf(nonHierarchicalReferencesType) && reference.isForward) { return true; } else if (referenceType.isSubtypeOf(organizesReferencesType) && !reference.isForward) { return true; } else if (connectsToReferenceType && referenceType.isSubtypeOf(connectsToReferenceType) && reference.isForward) { return true; } else if (referenceType.isSubtypeOf(hasEventSourceReferenceType) && reference.isForward) { return true; } return false; } const allReferences = node.allReferences(); const references = allReferences.filter(referenceToKeep); for (const reference of references.sort(sortByNodeId)) { if (getReferenceType(reference).browseName.toString() === "HasSubtype" && reference.isForward) { continue; } xw.startElement("Reference"); xw.writeAttribute("ReferenceType", b(xw, getReferenceType(reference).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) { const uax = getPrefix(xw, "http://opcfoundation.org/UA/2008/02/Types.xsd"); if (v.locale && v.locale.length) { xw.startElement(`${uax}Locale`); xw.text(v.locale); xw.endElement(); } xw.startElement(`${uax}Text`); if (v.text) { xw.text(v.text); } xw.endElement(); } function _dumpQualifiedName(xw: XmlWriter, v: QualifiedName) { const uax = getPrefix(xw, "http://opcfoundation.org/UA/2008/02/Types.xsd"); const t = translateBrowseName(xw, v); if (t.name) { xw.startElement(`${uax}Name`); xw.text(t.name); xw.endElement(); } if (t.namespaceIndex) { xw.startElement(`${uax}NamespaceIndex`); xw.text(t.namespaceIndex.toString()); xw.endElement(); } } function _dumpXmlElement(xw: XmlWriter, v: string) { xw.text(v); } /* i=339 1900-01-01T00:00:00Z */ type XmlNamespaceUri = string; type NamespaceUri = string; type XmlNs = string; interface XmlWriterEx extends XmlWriter { map: Record; stackMap: Record[]; namespaceArray: NamespaceUri[]; } function initXmlWriterEx(xw: XmlWriter, map: Record, namespaceArray: NamespaceUri[]): void { const xwe = xw as XmlWriterEx; xwe.map = map; xwe.stackMap = []; xwe.namespaceArray = namespaceArray; } function findXsdNamespaceUri(xw: XmlWriter, nodeId: NodeId): string { const xwe = xw as XmlWriterEx; if (!xwe.namespaceArray) { return ""; } const namespace = xwe.namespaceArray[nodeId.namespace]; if (namespace === "http://opcfoundation.org/UA/") { return "http://opcfoundation.org/UA/2008/02/Types.xsd"; } // c8 ignore next if (!namespace) { return ""; } return namespace.replace(/\/$/, "") + "/Types.xsd"; } function getPrefix(xw: XmlWriter, namespace: XmlNamespaceUri): XmlNs { const xwe = xw as XmlWriterEx; if (!xwe.map) return ""; const p = xwe.map[namespace] || ""; return p ? p + ":" : ""; } function restoreDefaultNamespace(xw: XmlWriter) { const xwe = xw as XmlWriterEx; if (!xwe.map) return; xwe.map = xwe.stackMap.pop()!; } function setDefaultNamespace(xw: XmlWriter, namespace: XmlNamespaceUri): void { const xwe = xw as XmlWriterEx; if (!xwe.map) return; if (xwe.map[namespace] !== "") { xw.writeAttribute("xmlns", namespace); } xwe.stackMap.push({ ...xwe.map }); xwe.map[namespace] = ""; } function startElementEx(xw: XmlWriter, ns: XmlNs, name: string, defaultNamespace: XmlNamespaceUri) { const xwe = xw as XmlWriterEx; xw.startElement(name); setDefaultNamespace(xw, defaultNamespace); } function _dumpNodeId(xw: XmlWriter, v: NodeId) { const xmlns = getPrefix(xw, "http://opcfoundation.org/UA/2008/02/Types.xsd"); xw.startElement(`${xmlns}Identifier`); xw.text(n(xw, v)); xw.endElement(); } function _dumpVariantValue( xw: XmlWriter, dataTypeNodeId: NodeId, dataType: DataType, addressSpace: IAddressSpace, value: any ) { if (value === undefined || value === null) { return; } if (dataType === DataType.Null) { return; } const uax = getPrefix(xw, "http://opcfoundation.org/UA/2008/02/Types.xsd"); xw.startElement(`${uax}${DataType[dataType]}`); const definitionMap = makeDefinitionMap(addressSpace); _dumpVariantInnerValue(xw, dataType, dataTypeNodeId, definitionMap, addressSpace, value); xw.endElement(); } function _dumpVariantInnerExtensionObject( xw: XmlWriter, definitionMap: DefinitionMap2, definition: StructureDefinition, addressSpace: IAddressSpace, value: ExtensionObject ) { const namespaceUri = findXsdNamespaceUri(xw, definition.defaultEncodingId); const ns = getPrefix(xw, namespaceUri); const isUnion = definition.structureType === StructureType.Union || definition.structureType === StructureType.UnionWithSubtypedValues; for (const field of definition.fields || []) { const dataTypeNodeId = field.dataType; const fieldName = field.name!; const lowerFieldName = lowerFirstLetter(fieldName); const v = (value as unknown as Record)[lowerFieldName]; if (v !== null && v !== undefined) { if ( dataTypeNodeId.namespace === 0 && dataTypeNodeId.value === 0 && dataTypeNodeId.identifierType === NodeIdType.NUMERIC ) { // to do ?? shall we do a extension Object here ? continue; // ns=0;i=0 is reserved } const { name, definition } = definitionMap.findDefinition(dataTypeNodeId); startElementEx(xw, ns, fieldName, namespaceUri); // xw.startElement(fieldName); let fun: (value: any) => void = (value: any) => { /** */ }; if (definition instanceof StructureDefinition) { fun = _dumpVariantInnerExtensionObject.bind(null, xw, definitionMap, definition, addressSpace); } else if (definition instanceof EnumDefinition) { fun = _dumpVariantInnerValueEnum.bind(null, xw, definition); } else if (definition?.dataType == DataType.Variant) { fun = (value: Variant)=> { xw.startElement("Value"); _dumpVariantValue(xw, field.dataType, value.dataType, addressSpace, value.value); xw.endElement(); } } else { const baseType = definition.dataType; fun = _dumpVariantInnerValue.bind(null, xw, baseType, dataTypeNodeId, definitionMap, addressSpace); } try { if (field.valueRank === -1) { fun(v); } else { // array for (const arrayItem of v as any[]) { xw.startElement(name); fun(arrayItem); xw.endElement(); } } } catch (err) { // eslint-disable-next-line max-depth if (types.isNativeError(err)) { errorLog("Error in _dumpVariantExtensionObjectValue_Body !!!", err.message); } debugLog(name); debugLog(field); // throw err; } restoreDefaultNamespace(xw); xw.endElement(); } else { if (!isUnion && !field.isOptional) { // field is mandatory but is null=> provide an empty array startElementEx(xw, ns, fieldName, namespaceUri); restoreDefaultNamespace(xw); xw.endElement(); } } } } function _dumpVariantInnerValueEnum(xw: XmlWriter, definition: EnumDefinition, value: any): void { if (!definition.fields) { return; } const field = definition.fields.find((f) => f.value[1] === value || f.name === value); xw.text(`${field?.name}_${field?.value[1]}`); } // eslint-disable-next-line complexity function _dumpVariantInnerValue( xw: XmlWriter, dataType: DataType, dataTypeNodeId: NodeId, definitionMap: DefinitionMap2, addressSpace: IAddressSpace, value: any ): void { const uax = getPrefix(xw, "http://opcfoundation.org/UA/2008/02/Types.xsd"); 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.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: xw.text(value.toString()); break; case DataType.ByteString: { const base64 = value.toString("base64"); xw.text(base64.length > 80 ? base64.match(/.{0,80}/g).join("\n") : base64); } break; case DataType.Guid: /* 947c29a7-490d-4dc9-adda-1109e3e8fcb7 */ if (value !== undefined && value !== null) { // xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2008/02/Types.xsd"); xw.startElement(`${uax}String`); xw.text(value.toString()); xw.endElement(); } break; case DataType.ExtensionObject: _dumpVariantExtensionObjectValue(xw, dataTypeNodeId, definitionMap, addressSpace, value as ExtensionObject); break; case DataType.XmlElement: _dumpXmlElement(xw, value as string); break; case DataType.StatusCode: xw.text((value as StatusCode).value.toString()); break; case DataType.Variant: _dumpVariantInnerValue(xw, value.dataType, dataTypeNodeId, definitionMap, addressSpace, value.value); break; default: errorLog("_dumpVariantInnerValue incomplete " + value + " " + "DataType=" + dataType + "=" + DataType[dataType]); // throw new Error("_dumpVariantInnerValue incomplete " + value + " " + "DataType=" + dataType + "=" + DataType[dataType]); } } /** * * @param xw * @param schema * @param value * @private */ function _dumpVariantExtensionObjectValue_Body( xw: XmlWriter, definitionMap: DefinitionMap2, name: string, definition: StructureDefinition, addressSpace: IAddressSpace, value: any ) { if (value) { const namespaceUri = findXsdNamespaceUri(xw, definition.defaultEncodingId); const ns = getPrefix(xw, namespaceUri); startElementEx(xw, ns, `${name}`, namespaceUri); if (value) { _dumpVariantInnerExtensionObject(xw, definitionMap, definition, addressSpace, value); } restoreDefaultNamespace(xw); xw.endElement(); } } function _dumpVariantExtensionObjectValue( xw: XmlWriter, dataTypeNodeId: NodeId, definitionMap: DefinitionMap2, addressSpace: IAddressSpace, value: ExtensionObject ) { const { name, definition } = definitionMap.findDefinition(dataTypeNodeId); // const encodingDefaultXml = (getStructureTypeConstructor(schema.name) as any).encodingDefaultXml; const encodingDefaultXml = value.schema.encodingDefaultXml; if (!encodingDefaultXml || encodingDefaultXml.isEmpty()) { warningLog("dataType Name ", name, "with ", dataTypeNodeId.toString(), " does not have xml encoding"); // throw new Error("Extension Object doesn't provide a XML "); return; } const uax = getPrefix(xw, "http://opcfoundation.org/UA/2008/02/Types.xsd"); startElementEx(xw, uax, `ExtensionObject`, "http://opcfoundation.org/UA/2008/02/Types.xsd"); { const uax = getPrefix(xw, "http://opcfoundation.org/UA/2008/02/Types.xsd"); xw.startElement(`${uax}TypeId`); { // find HasEncoding node // xx var encodingDefaultXml = schema.encodingDefaultXml; xw.startElement(`${uax}Identifier`); xw.text(n(xw, encodingDefaultXml)); xw.endElement(); } xw.endElement(); startElementEx(xw, uax, "Body", "http://opcfoundation.org/UA/2008/02/Types.xsd"); _dumpVariantExtensionObjectValue_Body( xw, definitionMap, name, definition as StructureDefinition, addressSpace, value); restoreDefaultNamespace(xw); xw.endElement(); } restoreDefaultNamespace(xw); xw.endElement(); } function _dumpVariantExtensionObjectValue2(xw: XmlWriter, addressSpace: IAddressSpace, value: ExtensionObject) { const dataTypeNodeId = value.schema.dataTypeNodeId; const definitionMap = makeDefinitionMap(addressSpace); const dataTypeNode = addressSpace.findDataType(dataTypeNodeId)!; if (!dataTypeNode) { warningLog("_dumpVariantExtensionObjectValue2: Cannot find dataType for ", dataTypeNodeId.toString()); return; } _dumpVariantExtensionObjectValue(xw, dataTypeNode.nodeId, definitionMap,addressSpace, value); } // eslint-disable-next-line complexity 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 || isMinDate(value.value)) { return true; } break; case DataType.ByteString: if (!value.value || value.value.length === 0) { return true; } break; case DataType.Boolean: return false; // we want it all the time ! 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.Int64: 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; } } // eslint-disable-next-line max-statements function _dumpValue(xw: XmlWriter, node: UAVariable | UAVariableType, variant: Variant) { const addressSpace = node.addressSpace; // c8 ignore next if (variant === null || variant === undefined) { return; } assert(variant instanceof Variant); const dataTypeNode = addressSpace.findDataType(node.dataType); // c8 ignore next if (!dataTypeNode) { debugLog("Cannot find dataType:", node.dataType.toString()); return; } const dataTypeName = dataTypeNode.browseName.name!.toString(); const baseDataTypeName = DataType[variant.dataType]; if (baseDataTypeName === "Null") { return; } assert(typeof baseDataTypeName === "string"); // determine if dataTypeName is a ExtensionObject const isExtensionObject = variant.dataType === DataType.ExtensionObject; if (_isDefaultValue(variant)) { return; } xw.startElement("Value"); const uax = getPrefix(xw, "http://opcfoundation.org/UA/2008/02/Types.xsd"); if (isExtensionObject) { const encodeXml = _dumpVariantExtensionObjectValue2.bind(null, xw, node.addressSpace); switch (variant.arrayType) { case VariantArrayType.Matrix: case VariantArrayType.Array: startElementEx(xw, uax, `ListOf${baseDataTypeName}`, "http://opcfoundation.org/UA/2008/02/Types.xsd"); variant.value.forEach(encodeXml); restoreDefaultNamespace(xw); xw.endElement(); break; case VariantArrayType.Scalar: encodeXml(variant.value); break; default: errorLog(node.toString()); errorLog("_dumpValue : unsupported arrayType: ", variant.arrayType); } } else { const encodeXml = _dumpVariantValue.bind(null, xw, node.dataType, variant.dataType, node.addressSpace); switch (variant.arrayType) { case VariantArrayType.Matrix: case VariantArrayType.Array: startElementEx(xw, uax, `ListOf${dataTypeName}`, "http://opcfoundation.org/UA/2008/02/Types.xsd"); variant.value.forEach(encodeXml); restoreDefaultNamespace(xw); xw.endElement(); break; case VariantArrayType.Scalar: encodeXml(variant.value); break; default: errorLog(node.toString()); errorLog("_dumpValue : unsupported arrayType: ", variant.arrayType); } } xw.endElement(); } function _dumpArrayDimensionsAttribute(xw: XmlWriter, node: UAVariableType | UAVariable) { if (node.arrayDimensions) { if (node.valueRank === -1 || (node.arrayDimensions.length === 1 && node.arrayDimensions[0] === 0)) { return; } xw.writeAttribute("ArrayDimensions", node.arrayDimensions.join(",")); } } function visitUANode(node: BaseNode, data: DumpData, forward: boolean) { const addressSpace = node.addressSpace; // visit references function process_reference(reference: UAReference) { // only backward or forward references if (reference.isForward !== forward) { return; } if (reference.nodeId.namespace === 0) { return; // skip OPCUA namespace } const k = _hash(reference); if (!data.index_el[k]) { data.index_el[k] = 1; const o = addressSpace.findNode(k)! as BaseNode; if (o) { visitUANode(o, data, forward); } } } (node as BaseNodeImpl).ownReferences().forEach(process_reference); data.elements.push(node as BaseNodeImpl); return node; } function dumpNodeInXml(xw: XmlWriter, node: BaseNode) { return (node as BaseNodeImpl).dumpXML(xw); } 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 = ReferenceImpl.resolveReferenceNode(addressSpace, r[0])! as BaseNode; if (!typeDefinitionObj) { warningLog(node.toString()); warningLog( "dumpReferencedNodes: Warning : " + node.browseName.toString() + " unknown typeDefinition, ", r[0].toString() ); } else { assert(typeDefinitionObj instanceof BaseNodeImpl); if (typeDefinitionObj.nodeId.namespace === node.nodeId.namespace) { // only output node if it is on the same namespace if (!xw.visitedNode.has(_hash(typeDefinitionObj))) { dumpNodeInXml(xw, typeDefinitionObj); } } } } } // { const r = node.findReferencesEx("HasSubtype", BrowseDirection.Inverse); if (r && r.length) { const subTypeOf = ReferenceImpl.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.has(_hash(subTypeOf))) { dumpNodeInXml(xw, subTypeOf); } } } } } else { const r = node.findReferencesEx("Aggregates", BrowseDirection.Forward); for (const reference of r) { const nodeChild = ReferenceImpl.resolveReferenceNode(addressSpace, reference) as BaseNode; assert(nodeChild instanceof BaseNodeImpl); if (nodeChild.nodeId.namespace === node.nodeId.namespace) { if (!xw.visitedNode.has(_hash(nodeChild))) { debugLog( node.nodeId.toString(), " dumping child ", nodeChild.browseName.toString(), nodeChild.nodeId.toString() ); dumpNodeInXml(xw, nodeChild); } } } } } function getParent(node: BaseNode): BaseNode | null { if (node instanceof UAVariableImpl || node instanceof UAMethodImpl || node instanceof UAObjectImpl) { return node.parent; } return null; } const currentReadFlag = makeAccessLevelFlag("CurrentRead"); function dumpCommonAttributes(xw: XmlWriter, node: BaseNode) { xw.writeAttribute("NodeId", n(xw, node.nodeId)); xw.writeAttribute("BrowseName", b(xw, node.browseName)); const parentNode = getParent(node); if (parentNode) { if (parentNode.nodeId.namespace <= node.nodeId.namespace) { xw.writeAttribute("ParentNodeId", n(xw, parentNode.nodeId)); } } if (Object.prototype.hasOwnProperty.call(node, "symbolicName")) { xw.writeAttribute("SymbolicName", (node as any).symbolicName); } if (Object.prototype.hasOwnProperty.call(node, "isAbstract")) { if ((node as any).isAbstract) { xw.writeAttribute("IsAbstract", (node as any).isAbstract ? "true" : "false"); } } if (Object.prototype.hasOwnProperty.call(node, "accessLevel")) { // CurrentRead is by default if ((node as UAVariable).accessLevel !== currentReadFlag) { xw.writeAttribute("AccessLevel", (node as UAVariable).accessLevel.toString()); } } if (Object.prototype.hasOwnProperty.call(node, "minimumSamplingInterval")) { const minimumSamplingInterval = (node as UAVariable).minimumSamplingInterval; if (minimumSamplingInterval > 0) { xw.writeAttribute("MinimumSamplingInterval", minimumSamplingInterval); } } } 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] === 0xffffffff && int64[1] === 0xffffffff) { return 0xffffffff; } if (int64[0] !== 0) { warningLog("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 (!isNullOrUndefined(defItem.value)) { xw.writeAttribute("Value", coerceInt64ToInt32(defItem.value)); } _dumpDescription(xw, defItem); xw.endElement(); } } function _dumpStructureDefinition( xw: XmlWriter, structureDefinition: StructureDefinition, baseStructureDefinition: StructureDefinition | null | undefined ) { /* * note: baseDataType and defaultEncodingId are implicit and not stored in the XML file ?? * */ // const baseDataType = structureDefinition.baseDataType; // const defaultEncodingId = structureDefinition.defaultEncodingId; // do not repeat elements that are already defined in base structure in the xml ouput! const fields = structureDefinition.fields || []; const nbFieldsInBase: number = baseStructureDefinition ? baseStructureDefinition.fields?.length || 0 : 0; for (let index = nbFieldsInBase; index < fields.length; index++) { const defItem = fields[index]; 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)); } _dumpDescription(xw, defItem); xw.endElement(); } } function _dumpEncoding(xw: XmlWriter, uaEncoding: UAObject) { const uaDescription = uaEncoding.findReferencesAsObject("HasDescription")[0]; if (uaDescription) { dumpUAVariable(xw, uaDescription as UAVariable); } _dumpUAObject(xw, uaEncoding); } function _dumpEncodings(xw: XmlWriter, uaDataType: UADataType) { const encodings = uaDataType.findReferencesExAsObject("HasEncoding", BrowseDirection.Forward); for (const uaEncoding of encodings) { if (uaEncoding.nodeClass !== NodeClass.Object) { continue; } _dumpEncoding(xw, uaEncoding as UAObject); } } function _dumpUADataTypeDefinition(xw: XmlWriter, uaDataType: UADataType) { const uaDataTypeBase = uaDataType.subtypeOfObj; if (uaDataType.isEnumeration()) { xw.startElement("Definition"); xw.writeAttribute("Name", b(xw, uaDataType.browseName)); _dumpEnumDefinition(xw, uaDataType.getEnumDefinition()); xw.endElement(); return; } if (uaDataType.isStructure()) { // in case the namespace is conforming to 1.03 specification the DataTypeDefinition attribute // will be not be readable.... const dataValue = uaDataType.readAttribute(SessionContext.defaultContext, AttributeIds.DataTypeDefinition); if (true || dataValue.statusCode.isGood()) { const definition = uaDataType.getStructureDefinition(); const baseDefinition = uaDataTypeBase ? uaDataTypeBase.getStructureDefinition() : null; xw.startElement("Definition"); xw.writeAttribute("Name", b(xw, uaDataType.browseName)); if (definition.structureType === StructureType.Union) { xw.writeAttribute("IsUnion", "true"); } _dumpStructureDefinition(xw, definition, baseDefinition); xw.endElement(); } return; } } function dumpUAView(xw: XmlWriter, node: UAView) { _markAsVisited(xw, node); xw.startElement("UAView"); xw.writeAttribute("NodeId", n(xw, node.nodeId)); xw.writeAttribute("BrowseName", b(xw, node.browseName)); dumpCommonElements(xw, node); xw.endElement(); dumpAggregates(xw, node); } 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"); } dumpCommonElements(xw, node); _dumpUADataTypeDefinition(xw, node); xw.endElement(); _dumpEncodings(xw, node); dumpAggregates(xw, node); } function _markAsVisited(xw: XmlWriter, node: BaseNode) { xw.visitedNode = xw.visitedNode || new Set(); assert(!xw.visitedNode.has(_hash(node))); xw.visitedNode.add(_hash(node)); } function dumpUAVariable(xw: XmlWriter, node: UAVariable) { assert(node.nodeClass === NodeClass.Variable); if (xw.visitedNode.has(_hash(node))) { return; } _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); const value = (node as UAVariableImpl).$dataValue.value; if (value) { _dumpValue(xw, node, value); } } xw.endElement(); dumpAggregates(xw, node); } function dumpUAVariableType(xw: XmlWriter, node: UAVariableType) { assert(node.nodeClass === NodeClass.VariableType); xw.visitedNode = xw.visitedNode || new Set(); assert(!xw.visitedNode.has(_hash(node))); xw.visitedNode.add(_hash(node)); 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); debugLog( " 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); const value = (node as UAVariableTypeImpl).value as Variant; if (value) { _dumpValue(xw, node, value); } } xw.endElement(); dumpAggregates(xw, node); } function dumpUAObject(xw: XmlWriter, node: UAObject) { xw.writeComment("Object - " + b(xw, node.browseName) + " {{{{ "); _dumpUAObject(xw, node); xw.writeComment("Object - " + b(xw, node.browseName) + " }}}} "); } function _dumpUAObject(xw: XmlWriter, node: UAObject) { assert(node.nodeClass === NodeClass.Object); xw.visitedNode = xw.visitedNode || new Set(); assert(!xw.visitedNode.has(_hash(node))); xw.visitedNode.add(_hash(node)); // 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 as UAObjectImpl); } function dumpElementInFolder(xw: XmlWriter, node: BaseNodeImpl) { const aggregates = node .getFolderElements() .sort((x: BaseNode, y: BaseNode) => (x.browseName.name!.toString() > y.browseName.name!.toString() ? 1 : -1)); for (const aggregate of aggregates.sort(sortByNodeId)) { // do not export node that do not belong to our namespace if (node.nodeId.namespace !== aggregate.nodeId.namespace) { return; } if (!xw.visitedNode.has(_hash(aggregate))) { aggregate.dumpXML(xw); } } } function dumpAggregates(xw: XmlWriter, node: BaseNode) { // Xx xw.writeComment("Aggregates {{ "); const aggregates = node.getAggregates().sort(sortByBrowseName); // const aggregates = node.findReferencesExAsObject("Aggregates", BrowseDirection.Forward); for (const aggregate of aggregates.sort(sortByNodeId)) { // do not export node that do not belong to our namespace if (node.nodeId.namespace !== aggregate.nodeId.namespace) { continue; } // even further! we should export here the children // that have been added later by a namespace with higher index if (_hasHigherPriorityThan(xw, aggregate.nodeId.namespace, node.nodeId.namespace)) { continue; } if (!xw.visitedNode.has(_hash(aggregate))) { (aggregate).dumpXML(xw); } } // Xx xw.writeComment("Aggregates }} "); } function dumpUAObjectType(xw: XmlWriter, node: UAObjectTypeImpl) { assert(node.nodeClass === NodeClass.ObjectType); assert(node instanceof UAObjectTypeImpl); xw.writeComment("ObjectType - " + b(xw, node.browseName) + " {{{{ "); _markAsVisited(xw, node); // dump SubtypeOf and HasTypeDefinition node if part of the same namespace 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) + " }}}}"); } function dumpUAMethod(xw: XmlWriter, node: UAMethod) { assert(node.nodeClass === NodeClass.Method); _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); } function resolveDataTypeName(addressSpace: IAddressSpace, dataType: string | NodeId): QualifiedName { let dataTypeNode = null; // c8 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) { errorLog("resolveDataTypeName: warning cannot find DataType " + dataType.toString()); return new QualifiedName({ name: "", namespaceIndex: 0 }); } return dataTypeNode.browseName; } function buildUpAliases(node: BaseNode, xw: XmlWriter, data: BuildAliasesData) { const addressSpace = node.addressSpace; if (!data.aliases_visited) data.aliases_visited = new Set(); const k = _hash(node); // c8 ignore next if (data.aliases_visited.has(k)) { return; } data.aliases_visited.add(k); // 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 (!data.aliases[dataTypeName]) { data.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 (!data.aliases[dataTypeName]) { data.aliases[dataTypeName] = n(xw, nodeV.dataType); } } } } function collectReferenceNameInAlias(reference: UAReference) { // reference.referenceType const key = b(xw, getReferenceType(reference).browseName); if (!data.aliases.key) { if (reference.referenceType.namespace === 0) { data.aliases[key] = reference.referenceType.toString().replace("ns=0;", ""); } else { data.aliases[key] = n(xw, reference.referenceType); } } } node.allReferences().forEach(collectReferenceNameInAlias); } function writeAliases(xw: XmlWriter, aliases: Record) { 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(); } function dumpReferenceType(xw: XmlWriter, referenceType: UAReferenceType) { _markAsVisited(xw, referenceType); xw.startElement("UAReferenceType"); dumpCommonAttributes(xw, referenceType); const isSymmetric = !referenceType.inverseName || referenceType.inverseName?.text === referenceType.browseName?.name; if (isSymmetric) { xw.writeAttribute("Symmetric", "true"); } dumpCommonElements(xw, referenceType); if (!isSymmetric) { xw.startElement("InverseName"); xw.text(referenceType.inverseName!.text || ""); xw.endElement(); } xw.endElement(); } export 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; } function sortByNodeId(a: { nodeId: NodeId }, b: { nodeId: NodeId }) { return a.nodeId.toString() < b.nodeId.toString() ? -1 : 1; } interface Dumpable { dumpXML(xw: typeof XMLWriter): void; } type NodeIdString = string; export interface BuildAliasesData { aliases: Record; aliases_visited?: Set; } interface DumpData extends BuildAliasesData { elements: Dumpable[]; index_el: Record; } export interface DumpXMLOptions { /** */ } UAMethodImpl.prototype.dumpXML = function (xw) { dumpUAMethod(xw, this); }; UAObjectImpl.prototype.dumpXML = function (xw) { dumpUAObject(xw, this); }; UAVariableImpl.prototype.dumpXML = function (xw: XmlWriter) { dumpUAVariable(xw, this); }; UAVariableTypeImpl.prototype.dumpXML = function (xw) { dumpUAVariableType(xw, this); }; UAReferenceTypeImpl.prototype.dumpXML = function (xw: XmlWriter) { dumpReferenceType(xw, this); }; UAObjectTypeImpl.prototype.dumpXML = function (xw) { dumpUAObjectType(xw, this); }; UADataTypeImpl.prototype.dumpXML = function (xw: XmlWriter) { dumpUADataType(xw, this); }; UAViewImpl.prototype.dumpXML = function (xw: XmlWriter) { dumpUAView(xw, this); }; function makeTypeXsd(namespaceUri: string): string { return namespaceUri.replace(/\/$/, "") + "/Type.xsd"; } // eslint-disable-next-line max-statements NamespaceImpl.prototype.toNodeset2XML = function (this: NamespaceImpl) { const namespaceArrayNode = this.addressSpace.findNode(VariableIds.Server_NamespaceArray); const namespaceArray: string[] = namespaceArrayNode ? namespaceArrayNode.readAttribute(null, AttributeIds.Value).value.value : []; const xw: XmlWriter = new XMLWriter(true); xw.priorityTable = constructNamespacePriorityTable(this.addressSpace).priorityTable; const dependency = constructNamespaceDependency(this, xw.priorityTable); const translationTable = _constructNamespaceTranslationTable(dependency, this); 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"); const namespacesMap: Record = { "http://opcfoundation.org/UA/2011/03/UANodeSet.xsd": "", "http://opcfoundation.org/UA/2008/02/Types.xsd": "uax", "http://www.w3.org/2001/XMLSchema-instance": "xsi" }; for (const namespace of dependency) { if (namespace.index === 0) { continue; } const translatedIndex = translationTable.get(namespace.index); const smallName = `ns${translatedIndex}`; xw.writeAttribute(`xmlns:${smallName}`, makeTypeXsd(namespace.namespaceUri)); namespacesMap[namespace.namespaceUri] = smallName; } // xx xw.writeAttribute("Version", "1.02"); // xx xw.writeAttribute("LastModified", (new Date()).toISOString()); // ------------- INamespace Uris initXmlWriterEx(xw, namespacesMap, namespaceArray); xw.startElement("NamespaceUris"); // let's sort the dependencies in the same order as the translation table const sortedDependencies = dependency.sort( (a, b) => (translationTable.get(a.index)! > translationTable.get(b.index)! ? 1 : -1)); doDebug && debugLog(sortedDependencies.map((a) => a.index + " + " + a.namespaceUri).join("\n")); doDebug && debugLog("translation table ", translationTable.entries()); for (const depend of sortedDependencies) { if (depend.index === 0) { continue; // ignore namespace 0 } xw.startElement("Uri"); xw.text(depend.namespaceUri); xw.endElement(); } xw.endElement(); // ------------- INamespace Uris xw.startElement("Models"); { xw.startElement("Model"); xw.writeAttribute("ModelUri", this.namespaceUri); xw.writeAttribute("Version", this.version); xw.writeAttribute("PublicationDate", this.publicationDate.toISOString()); for (const depend of sortedDependencies) { if (depend.index === this.index) { continue; // ignore our namespace 0 } xw.startElement("RequiredModel"); xw.writeAttribute("ModelUri", depend.namespaceUri); xw.writeAttribute("Version", depend.version); xw.writeAttribute("PublicationDate", depend.publicationDate.toISOString()); xw.endElement(); } xw.endElement(); } xw.endElement(); const data: BuildAliasesData = { aliases: {} }; for (const node of this.nodeIterator()) { buildUpAliases(node, xw, data); } writeAliases(xw, data.aliases); xw.visitedNode = new Set(); // -------------- writeReferences xw.writeComment("ReferenceTypes"); const referenceTypes = [...this._referenceTypeIterator()].sort(sortByBrowseName); for (const referenceType of referenceTypes) { dumpReferenceType(xw, referenceType); } // -------------- Dictionaries const addressSpace = this.addressSpace; const getDataTypeDescription = (opcBinaryTypeSystem: UAObject) => { 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); return result; }; // -------------- 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.sort(sortByNodeId)) { if (!xw.visitedNode.has(_hash(dataType))) { dumpNodeInXml(xw, dataType); } } // -------------- const opcBinaryTypeSystem = addressSpace.findNode(ObjectIds.OPCBinarySchema_TypeSystem) as UAObject; const opcXmlSchemaTypeSystem = addressSpace.findNode(ObjectIds.XmlSchema_TypeSystem) as UAObject; if (opcBinaryTypeSystem) { // let find all DataType dictionary node corresponding to a given namespace // (have DataTypeDictionaryType) const result = getDataTypeDescription(opcBinaryTypeSystem); if (result.length === 1) { xw.writeComment("DataSystem - Binary"); const dataSystemType = addressSpace.findNode(result[0].nodeId)! as UAVariable; const types = dataSystemType.getComponents(); for (const f of types) { dumpNodeInXml(xw, f); } dumpNodeInXml(xw, dataSystemType); } } if (opcXmlSchemaTypeSystem) { const result = getDataTypeDescription(opcXmlSchemaTypeSystem); if (result.length === 1) { xw.writeComment("DataSystem - Xml"); const dataSystemType = addressSpace.findNode(result[0].nodeId)! as UAVariable; const types = dataSystemType.getComponents(); for (const f of types) { dumpNodeInXml(xw, f); } dumpNodeInXml(xw, dataSystemType); } } } // -------------- 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.sort(sortByNodeId)) { if (!xw.visitedNode.has(_hash(objectType))) { dumpNodeInXml(xw, objectType); } } // -------------- 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.sort(sortByNodeId)) { if (!xw.visitedNode.has(_hash(variableType))) { dumpNodeInXml(xw, variableType); } } // -------------- Any thing else xw.writeComment("Other Nodes"); const nodes = [...this.nodeIterator()].sort(sortByBrowseName); for (const node of nodes.sort(sortByNodeId)) { if (!xw.visitedNode.has(_hash(node))) { dumpNodeInXml(xw, node); } } xw.endElement(); xw.endDocument(); return xw.toString(); };