/**
* @module node-opcua-address-space
*/
// produce nodeset xml files
import { types } from "node:util";
import type {
BaseNode,
IAddressSpace,
UADataType,
UAMethod,
UAObject,
UAReference,
UAReferenceType,
UAVariable,
UAVariableType,
UAView
} from "node-opcua-address-space-base";
import { assert } from "node-opcua-assert";
import { AttributeIds, type Int64, isMinDate, type StatusCode } from "node-opcua-basic-types";
import { VariableIds } from "node-opcua-constants";
import { type LocalizedText, makeAccessLevelFlag, NodeClass, QualifiedName } from "node-opcua-data-model";
import { make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
import type { ExtensionObject } from "node-opcua-extension-object";
import { NodeId, NodeIdType } from "node-opcua-nodeid";
import { EnumDefinition, StructureDefinition, StructureType } from "node-opcua-types";
import { isNullOrUndefined, lowerFirstLetter } from "node-opcua-utils";
import { DataType, Variant, VariantArrayType } from "node-opcua-variant";
import XMLWriter from "xml-writer";
import { makeDefinitionMap } from "../../api/loader/decode_xml_extension_object.js";
import type { DefinitionMap2 } from "../../api/loader/make_xml_extension_object_parser.js";
import { SessionContext } from "../../api/session_context.js";
import type { XmlWriter } from "../../api/xml_writer.js";
import { BaseNodeImpl, getReferenceType } from "../base_node_impl.js";
import { NamespaceImpl } from "../namespace_impl.js";
import { ReferenceImpl } from "../reference_impl.js";
import { UAMethodImpl } from "../ua_method_impl.js";
import { UAObjectImpl } from "../ua_object_impl.js";
import { UAObjectTypeImpl } from "../ua_object_type_impl.js";
import { UAVariableImpl } from "../ua_variable_impl.js";
import type { UAVariableTypeImpl } from "../ua_variable_type_impl.js";
import {
_constructNamespaceTranslationTable,
constructNamespaceDependency,
constructNamespacePriorityTable
} from "./construct_namespace_dependency.js";
import { type NodesetWalkEvent, namespaceToWalkEvents, nodeToWalkEvents, sortByNodeId } from "./nodeset_to_records.js";
const debugLog = make_debugLog("nodeset_to_xml");
const warningLog = make_warningLog("nodeset_to_xml");
const errorLog = make_errorLog("nodeset_to_xml");
const doDebug = false;
function _hash(node: BaseNode | UAReference): string {
return node.nodeId.toString();
}
function _dumpDisplayName(xw: XmlWriter, node: BaseNode): void {
if (node.displayName?.[0]) {
xw.startElement("DisplayName")
.text(node.displayName[0].text || "")
.endElement();
}
}
function _dumpDescription(xw: XmlWriter, node: { description?: LocalizedText }): void {
if (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 _mustFindReferenceType(addressSpace: IAddressSpace, name: string): UAReferenceType {
const referenceType = addressSpace.findReferenceType(name);
// c8 ignore next
if (!referenceType) {
throw new Error(`Cannot find standard reference type ${name}: please check your nodeset file`);
}
return referenceType;
}
function _dumpReferences(xw: XmlWriter, node: BaseNode) {
xw.startElement("References");
const addressSpace = node.addressSpace;
const aggregateReferenceType = _mustFindReferenceType(addressSpace, "Aggregates");
// const hasChildReferenceType = _mustFindReferenceType(addressSpace, "HasChild");
const hasSubtypeReferenceType = _mustFindReferenceType(addressSpace, "HasSubtype");
const hasTypeDefinitionReferenceType = _mustFindReferenceType(addressSpace, "HasTypeDefinition");
const nonHierarchicalReferencesType = _mustFindReferenceType(addressSpace, "NonHierarchicalReferences");
const organizesReferencesType = _mustFindReferenceType(addressSpace, "Organizes");
// ConnectsTo is genuinely optional: not every nodeset registers it, and the check
// below already tolerates it being absent.
const connectsToReferenceType = addressSpace.findReferenceType("ConnectsTo");
const hasEventSourceReferenceType = _mustFindReferenceType(addressSpace, "HasEventSource");
function referenceToKeep(reference: UAReference): boolean {
// resolved here: a reference nobody has looked at since the load carries only the NodeId of its type
const referenceType = ReferenceImpl.resolveReferenceType(addressSpace, reference);
if (!referenceType) {
return false;
}
const targetedNamespaceIndex = reference.nodeId.namespace;
// 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) {
if (_hasHigherPriorityThan(xw, targetedNamespaceIndex, node.nodeId.namespace)) {
return false;
}
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) {
// e.g. HasInterface — always keep, the current node owns this reference
return true;
} else if (referenceType.isSubtypeOf(organizesReferencesType) && !reference.isForward) {
// Organizes inverse — the current node is organized by an external folder
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?.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[];
}
export 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;
const previousMap = xwe.stackMap.pop();
if (previousMap) {
xwe.map = previousMap;
}
}
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: unknown) {
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: unknown) => void = (_value: unknown) => {
/** */
};
if (definition instanceof StructureDefinition) {
fun = _dumpVariantInnerExtensionObject.bind(null, xw, definitionMap, definition, addressSpace) as (
value: unknown
) => void;
} else if (definition instanceof EnumDefinition) {
fun = _dumpVariantInnerValueEnum.bind(null, xw, definition);
} else if (definition?.dataType === DataType.Variant) {
fun = (value: unknown) => {
const variantValue = value as Variant;
xw.startElement("Value");
_dumpVariantValue(xw, field.dataType, variantValue.dataType, addressSpace, variantValue.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 unknown[]) {
xw.startElement(name);
fun(arrayItem);
xw.endElement();
}
}
} catch (err) {
if (types.isNativeError(err)) {
errorLog("Error in _dumpVariantExtensionObjectValue_Body !!!", err.message);
}
// c8 ignore next
if (doDebug) {
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: unknown): 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]}`);
}
function _dumpVariantInnerValue(
xw: XmlWriter,
dataType: DataType,
dataTypeNodeId: NodeId,
definitionMap: DefinitionMap2,
addressSpace: IAddressSpace,
value: unknown
): 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 as Date).toISOString());
break;
case DataType.Int64:
case DataType.UInt64:
xw.text((value as [number, number])[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 as { toString(): string }).toString());
break;
case DataType.ByteString:
{
const base64 = (value as Buffer).toString("base64");
const chunks = base64.match(/.{0,80}/g);
xw.text(base64.length > 80 && chunks ? chunks.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 as { toString(): string }).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: {
const variantValue = value as Variant;
_dumpVariantInnerValue(xw, variantValue.dataType, dataTypeNodeId, definitionMap, addressSpace, variantValue.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
*/
export function _dumpVariantExtensionObjectValue_Body(
xw: XmlWriter,
definitionMap: DefinitionMap2,
name: string,
definition: StructureDefinition,
addressSpace: IAddressSpace,
value: ExtensionObject
) {
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);
}
export 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;
}
}
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) {
// c8 ignore next
doDebug && 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 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.hasOwn(node, "symbolicName")) {
xw.writeAttribute("SymbolicName", (node as unknown as { symbolicName: string }).symbolicName);
}
if (Object.hasOwn(node, "isAbstract")) {
const isAbstract = (node as unknown as { isAbstract: boolean }).isAbstract;
if (isAbstract) {
xw.writeAttribute("IsAbstract", isAbstract ? "true" : "false");
}
}
// AccessLevel and UserAccessLevel exist in the XSD on UAVariable only. Gate on the node
// class rather than on the presence of the property: UAVariableTypeImpl happens not to own
// an accessLevel today, but it does own a `historizing` (see #1550), so a property-presence
// guard is one refactor away from emitting an attribute the schema rejects.
if (node.nodeClass === NodeClass.Variable) {
const variable = node as UAVariable;
// CurrentRead is by default
if (variable.accessLevel !== currentReadFlag) {
xw.writeAttribute("AccessLevel", variable.accessLevel.toString());
}
// UserAccessLevel is implicitly accessLevel when omitted (see convertUserAccessLevel in the loader),
// so it only needs to be written down when it further restricts accessLevel.
if (variable.userAccessLevel !== undefined && variable.userAccessLevel !== variable.accessLevel) {
xw.writeAttribute("UserAccessLevel", variable.userAccessLevel.toString());
}
}
// access policy: undefined means "inherit from the namespace", an empty rolePermissions array
// means "this node deliberately grants nothing".
if (node.accessRestrictions !== undefined) {
xw.writeAttribute("AccessRestrictions", node.accessRestrictions.toString());
}
if (node.rolePermissions !== undefined && node.rolePermissions.length === 0) {
xw.writeAttribute("HasNoPermissions", "true");
}
if (Object.hasOwn(node, "minimumSamplingInterval")) {
const minimumSamplingInterval = (node as UAVariable).minimumSamplingInterval;
if (minimumSamplingInterval > 0) {
xw.writeAttribute("MinimumSamplingInterval", minimumSamplingInterval);
}
}
// Historizing exists in the XSD on UAVariable only: UAVariableType is restricted to
// DataType/ValueRank/ArrayDimensions. UAVariableTypeImpl nevertheless owns a `historizing`
// property, so the node class must be tested here rather than the presence of the property.
// false being the default, the attribute is only emitted when it is set.
if (node.nodeClass === NodeClass.Variable && (node as UAVariable).historizing) {
xw.writeAttribute("Historizing", "true");
}
}
// the UANode XSD sequence is DisplayName, Description, Category, Documentation, References,
// RolePermissions, Extensions: RolePermissions must therefore come after References.
function _dumpRolePermissions(xw: XmlWriter, node: BaseNode) {
const rolePermissions = node.rolePermissions;
if (!rolePermissions || rolePermissions.length === 0) {
// the empty case is carried by the HasNoPermissions attribute
return;
}
xw.startElement("RolePermissions");
for (const rolePermission of rolePermissions) {
xw.startElement("RolePermission");
xw.writeAttribute("Permissions", rolePermission.permissions.toString());
xw.text(n(xw, rolePermission.roleId));
xw.endElement();
}
xw.endElement();
}
function dumpCommonElements(xw: XmlWriter, node: BaseNode) {
_dumpDisplayName(xw, node);
_dumpDescription(xw, node);
_dumpReferences(xw, node);
_dumpRolePermissions(xw, node);
}
export 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 as string);
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 _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);
const t = true;
if (t || 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();
}
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();
}
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);
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();
}
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));
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);
// c8 ignore next
doDebug &&
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();
}
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
xw.startElement("UAObject");
dumpCommonAttributes(xw, node);
dumpCommonElements(xw, node);
xw.endElement();
// dump aggregates nodes ( Properties / components )
}
function dumpUAObjectType(xw: XmlWriter, node: UAObjectTypeImpl) {
assert(node.nodeClass === NodeClass.ObjectType);
assert(node instanceof UAObjectTypeImpl);
_markAsVisited(xw, node);
// dump SubtypeOf and HasTypeDefinition node if part of the same namespace
xw.startElement("UAObjectType");
dumpCommonAttributes(xw, node);
dumpCommonElements(xw, node);
xw.endElement();
}
function dumpUAMethod(xw: XmlWriter, node: UAMethod) {
assert(node.nodeClass === NodeClass.Method);
_markAsVisited(xw, node);
xw.startElement("UAMethod");
dumpCommonAttributes(xw, node);
if (node.methodDeclarationId) {
xw.writeAttribute("MethodDeclarationId", n(xw, node.methodDeclarationId));
}
dumpCommonElements(xw, node);
xw.endElement();
}
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 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;
}
type NodeIdString = string;
export type DumpXMLOptions = Record;
/** the element of one node, and nothing else: the walk decides what comes before and after it */
function dumpElement(xw: XmlWriter, node: BaseNode): void {
switch (node.nodeClass) {
case NodeClass.Method:
dumpUAMethod(xw, node as UAMethod);
break;
case NodeClass.Object:
_dumpUAObject(xw, node as UAObject);
break;
case NodeClass.Variable:
dumpUAVariable(xw, node as UAVariable);
break;
case NodeClass.VariableType:
dumpUAVariableType(xw, node as UAVariableType);
break;
case NodeClass.ReferenceType:
dumpReferenceType(xw, node as UAReferenceType);
break;
case NodeClass.ObjectType:
dumpUAObjectType(xw, node as UAObjectTypeImpl);
break;
case NodeClass.DataType:
dumpUADataType(xw, node as UADataType);
break;
case NodeClass.View:
dumpUAView(xw, node as UAView);
break;
default:
throw new Error(`dumpElement: unexpected node class ${node.nodeClass}`);
}
}
/** the walk's events written as XML: comments, and each node's element looked up back in the address space */
function writeWalkEvents(
xw: XmlWriter,
addressSpace: IAddressSpace,
translationTable: Map,
events: NodesetWalkEvent[]
): void {
const reverse = new Map();
for (const [index, translated] of translationTable) {
reverse.set(translated, index);
}
xw.visitedNode = xw.visitedNode || new Set();
for (const event of events) {
if (event.kind === "section") {
xw.writeComment(event.text);
continue;
}
if (event.kind !== "node") {
continue;
}
const namespace = reverse.get(event.nodeId.namespace);
if (namespace === undefined) {
throw new Error(`toNodeset2XML: no namespace for ${event.nodeId.toString()}`);
}
const node = addressSpace.findNode(new NodeId(event.nodeId.identifierType, event.nodeId.value, namespace));
if (!node) {
throw new Error(`toNodeset2XML: cannot find ${event.nodeId.toString()} (namespace ${namespace})`);
}
dumpElement(xw, node);
}
}
/**
* one node as XML, with what the export brings with it: its type definition and supertype when they
* belong to the same namespace, its aggregates after it. The same walk toNodeset2XML runs, from this node.
*/
BaseNodeImpl.prototype.dumpXML = function (this: BaseNodeImpl, xw: XmlWriter) {
const { events, translationTable } = nodeToWalkEvents(this);
xw.translationTable = xw.translationTable || translationTable;
writeWalkEvents(xw, this.addressSpace, translationTable, events);
};
export function makeTypeXsd(namespaceUri: string): string {
return `${namespaceUri.replace(/\/$/, "")}/Type.xsd`;
}
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;
// the walk: the same records the image writer and the loader consume, plus the comments
const events = namespaceToWalkEvents(this);
const header = events[0];
if (header?.kind !== "header") {
throw new Error("toNodeset2XML: the walk must start with the header record");
}
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;
}
initXmlWriterEx(xw, namespacesMap, namespaceArray);
xw.startElement("NamespaceUris");
for (const namespaceUri of header.namespaceUris) {
xw.startElement("Uri");
xw.text(namespaceUri);
xw.endElement();
}
xw.endElement();
xw.startElement("Models");
for (const model of header.models) {
xw.startElement("Model");
xw.writeAttribute("ModelUri", model.modelUri);
xw.writeAttribute("Version", model.version);
xw.writeAttribute("PublicationDate", (model.publicationDate ?? this.publicationDate).toISOString());
for (const required of model.requiredModels) {
xw.startElement("RequiredModel");
xw.writeAttribute("ModelUri", required.modelUri);
xw.writeAttribute("Version", required.version);
xw.writeAttribute("PublicationDate", required.publicationDate.toISOString());
xw.endElement();
}
xw.endElement();
}
xw.endElement();
const aliases: Record = {};
for (const [name, nodeId] of Object.entries(header.aliases)) {
aliases[name] = nodeId.toString().replace("ns=0;", "");
}
writeAliases(xw, aliases);
xw.visitedNode = new Set();
writeWalkEvents(xw, this.addressSpace, translationTable, events.slice(1));
xw.endElement();
xw.endDocument();
return xw.toString();
};