node-opcua-address-space 2.181.1 → 2.182.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/helpers/ensure_secure_access.ts +53 -2
- package/api/index.ts +30 -1
- package/api/loader/generateAddressSpaceRaw.ts +44 -139
- package/api/loader/load_nodeset2.ts +5 -0
- package/api/loader/narrow_instance_datatypes.ts +264 -0
- package/api/loader/nodeset_builtin_formats.ts +143 -0
- package/api/loader/nodeset_format.ts +153 -0
- package/api/loader/nodeset_image.ts +37 -4
- package/api/loader/nodeset_image_codec.ts +153 -34
- package/api/loader/nodeset_record.ts +26 -2
- package/api/loader/nodeset_record_applier.ts +54 -3
- package/api/loader/nodeset_records_to_xml.ts +620 -0
- package/api/loader/nodeset_source.ts +185 -26
- package/api/loader/nodeset_to_image.ts +16 -5
- package/api/loader/nodeset_xml_header.ts +119 -0
- package/api/loader/nodeset_xml_primitives.ts +209 -0
- package/api/loader/nodeset_xml_producer.ts +61 -2
- package/api/loader/parsers/variant_parser.ts +9 -0
- package/bin/nodeset_equivalence.js +750 -0
- package/bin/nodeset_tool_common.js +72 -0
- package/bin/opcua-nodeset-image.js +93 -103
- package/bin/opcua-nodeset-live.js +139 -0
- package/dist/api/helpers/ensure_secure_access.d.ts +20 -0
- package/dist/api/helpers/ensure_secure_access.js +39 -0
- package/dist/api/helpers/ensure_secure_access.js.map +1 -1
- package/dist/api/index.d.ts +7 -2
- package/dist/api/index.js +21 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/loader/generateAddressSpaceRaw.d.ts +1 -7
- package/dist/api/loader/generateAddressSpaceRaw.js +32 -125
- package/dist/api/loader/generateAddressSpaceRaw.js.map +1 -1
- package/dist/api/loader/load_nodeset2.js +4 -0
- package/dist/api/loader/load_nodeset2.js.map +1 -1
- package/dist/api/loader/narrow_instance_datatypes.d.ts +30 -0
- package/dist/api/loader/narrow_instance_datatypes.js +217 -0
- package/dist/api/loader/narrow_instance_datatypes.js.map +1 -0
- package/dist/api/loader/nodeset_builtin_formats.d.ts +27 -0
- package/dist/api/loader/nodeset_builtin_formats.js +133 -0
- package/dist/api/loader/nodeset_builtin_formats.js.map +1 -0
- package/dist/api/loader/nodeset_format.d.ts +123 -0
- package/dist/api/loader/nodeset_format.js +63 -0
- package/dist/api/loader/nodeset_format.js.map +1 -0
- package/dist/api/loader/nodeset_image.d.ts +17 -1
- package/dist/api/loader/nodeset_image.js +32 -4
- package/dist/api/loader/nodeset_image.js.map +1 -1
- package/dist/api/loader/nodeset_image_codec.d.ts +4 -0
- package/dist/api/loader/nodeset_image_codec.js +142 -53
- package/dist/api/loader/nodeset_image_codec.js.map +1 -1
- package/dist/api/loader/nodeset_record.d.ts +26 -2
- package/dist/api/loader/nodeset_record.js +11 -2
- package/dist/api/loader/nodeset_record.js.map +1 -1
- package/dist/api/loader/nodeset_record_applier.js +52 -3
- package/dist/api/loader/nodeset_record_applier.js.map +1 -1
- package/dist/api/loader/nodeset_records_to_xml.d.ts +16 -0
- package/dist/api/loader/nodeset_records_to_xml.js +536 -0
- package/dist/api/loader/nodeset_records_to_xml.js.map +1 -0
- package/dist/api/loader/nodeset_source.d.ts +68 -7
- package/dist/api/loader/nodeset_source.js +157 -20
- package/dist/api/loader/nodeset_source.js.map +1 -1
- package/dist/api/loader/nodeset_to_image.d.ts +4 -4
- package/dist/api/loader/nodeset_to_image.js +14 -4
- package/dist/api/loader/nodeset_to_image.js.map +1 -1
- package/dist/api/loader/nodeset_xml_header.d.ts +22 -0
- package/dist/api/loader/nodeset_xml_header.js +113 -0
- package/dist/api/loader/nodeset_xml_header.js.map +1 -0
- package/dist/api/loader/nodeset_xml_primitives.d.ts +37 -0
- package/dist/api/loader/nodeset_xml_primitives.js +196 -0
- package/dist/api/loader/nodeset_xml_primitives.js.map +1 -0
- package/dist/api/loader/nodeset_xml_producer.js +56 -1
- package/dist/api/loader/nodeset_xml_producer.js.map +1 -1
- package/dist/api/loader/parsers/variant_parser.js +7 -0
- package/dist/api/loader/parsers/variant_parser.js.map +1 -1
- package/dist/impl/_instantiate_helpers.js +1 -0
- package/dist/impl/_instantiate_helpers.js.map +1 -1
- package/dist/impl/alarms_and_conditions/ua_alarm_condition_impl.js +4 -0
- package/dist/impl/alarms_and_conditions/ua_alarm_condition_impl.js.map +1 -1
- package/dist/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.js +14 -0
- package/dist/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.js.map +1 -1
- package/dist/impl/base_node_impl.d.ts +9 -0
- package/dist/impl/base_node_impl.js +11 -0
- package/dist/impl/base_node_impl.js.map +1 -1
- package/dist/impl/base_node_private.js +67 -97
- package/dist/impl/base_node_private.js.map +1 -1
- package/dist/impl/nodeset_tools/construct_namespace_dependency.js +5 -1
- package/dist/impl/nodeset_tools/construct_namespace_dependency.js.map +1 -1
- package/dist/impl/nodeset_tools/nodeset_to_records.js +61 -15
- package/dist/impl/nodeset_tools/nodeset_to_records.js.map +1 -1
- package/dist/impl/nodeset_tools/nodeset_to_xml.d.ts +1 -8
- package/dist/impl/nodeset_tools/nodeset_to_xml.js +174 -190
- package/dist/impl/nodeset_tools/nodeset_to_xml.js.map +1 -1
- package/dist/impl/ua_data_type_impl.d.ts +21 -0
- package/dist/impl/ua_data_type_impl.js +27 -2
- package/dist/impl/ua_data_type_impl.js.map +1 -1
- package/dist/impl/ua_object_type_impl.d.ts +9 -0
- package/dist/impl/ua_object_type_impl.js +12 -1
- package/dist/impl/ua_object_type_impl.js.map +1 -1
- package/dist/impl/ua_variable_impl.js +27 -10
- package/dist/impl/ua_variable_impl.js.map +1 -1
- package/dist/impl/ua_variable_impl_ext_obj.js +19 -2
- package/dist/impl/ua_variable_impl_ext_obj.js.map +1 -1
- package/dist/impl/ua_variable_type_impl.d.ts +8 -1
- package/dist/impl/ua_variable_type_impl.js +39 -2
- package/dist/impl/ua_variable_type_impl.js.map +1 -1
- package/dist/tsconfig_base.tsbuildinfo +1 -1
- package/distNodeJS/generate_address_space.js +4 -0
- package/distNodeJS/generate_address_space.js.map +1 -1
- package/impl/_instantiate_helpers.ts +3 -0
- package/impl/alarms_and_conditions/ua_alarm_condition_impl.ts +10 -0
- package/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.ts +17 -0
- package/impl/base_node_impl.ts +15 -0
- package/impl/base_node_private.ts +97 -146
- package/impl/nodeset_tools/construct_namespace_dependency.ts +5 -1
- package/impl/nodeset_tools/nodeset_to_records.ts +66 -16
- package/impl/nodeset_tools/nodeset_to_xml.ts +127 -160
- package/impl/ua_data_type_impl.ts +40 -2
- package/impl/ua_object_type_impl.ts +16 -3
- package/impl/ua_variable_impl.ts +27 -10
- package/impl/ua_variable_impl_ext_obj.ts +20 -2
- package/impl/ua_variable_type_impl.ts +48 -2
- package/package.json +29 -27
- package/source_nodejs/generate_address_space.ts +7 -1
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-address-space
|
|
3
|
+
*
|
|
4
|
+
* Records back to a NodeSet2 XML document, with no address space anywhere in the loop.
|
|
5
|
+
*
|
|
6
|
+
* This is the other half of {@link makeXmlNodesetRecordReader}: that one reads a NodeSet2 document
|
|
7
|
+
* into records, this one writes records back out as one. Together they make the conversion between
|
|
8
|
+
* NodeSet2 XML and NodeSet-NDJSON a transcription between two spellings of the same document,
|
|
9
|
+
* which is what the format claims to be.
|
|
10
|
+
*
|
|
11
|
+
* The alternative is to load the records into an address space and export that namespace
|
|
12
|
+
* (`NamespaceImpl.toNodeset2XML`). It answers a different question, and it is lossy in the way an
|
|
13
|
+
* information model is bound to be: the address space keeps what it needs to serve a session, not
|
|
14
|
+
* what a document happened to say, so anything it does not model has to be carried alongside the
|
|
15
|
+
* nodes for the sake of the export alone. This writer needs none of that, because it never leaves
|
|
16
|
+
* the document.
|
|
17
|
+
*
|
|
18
|
+
* Two things fall out of staying inside the document, and they are the whole reason it is exact:
|
|
19
|
+
*
|
|
20
|
+
* - **No namespace translation.** A record carries the indices of the file's own table, and this
|
|
21
|
+
* writer declares that same table, so the mapping is the identity. The address-space path has to
|
|
22
|
+
* rebuild a table out of a namespace and its dependencies, which is where a namespace can be
|
|
23
|
+
* dropped, added, or reordered.
|
|
24
|
+
* - **No DataType definitions.** A value reaches a record either as one of the four types the XML
|
|
25
|
+
* reader decodes (Argument, EUInformation, Range, EnumValueType) or as the XML it could not
|
|
26
|
+
* decode, kept verbatim. Nothing else can be in there -- the image codec refuses to carry a
|
|
27
|
+
* decoded anything-else -- so writing a value back never needs to look a structure up.
|
|
28
|
+
*/
|
|
29
|
+
import type { Int64 } from "node-opcua-basic-types";
|
|
30
|
+
import { NodeClass } from "node-opcua-data-model";
|
|
31
|
+
import { make_errorLog } from "node-opcua-debug";
|
|
32
|
+
import type { ExtensionObject } from "node-opcua-extension-object";
|
|
33
|
+
import type { NodeId } from "node-opcua-nodeid";
|
|
34
|
+
import { Argument, EnumValueType, EUInformation, Range } from "node-opcua-types";
|
|
35
|
+
import { DataType, VariantArrayType, type VariantOptions } from "node-opcua-variant";
|
|
36
|
+
import XMLWriter from "xml-writer";
|
|
37
|
+
import type { XmlWriter } from "../xml_writer.js";
|
|
38
|
+
import type { NodesetHeaderRecord, NodesetNodeRecord, NodesetRecord, NodesetReferenceRecord } from "./nodeset_record.js";
|
|
39
|
+
import { XmlExtensionObjectFragment } from "./nodeset_record.js";
|
|
40
|
+
import {
|
|
41
|
+
_dumpLocalizedText,
|
|
42
|
+
_dumpNodeId,
|
|
43
|
+
_dumpQualifiedName,
|
|
44
|
+
b,
|
|
45
|
+
getPrefix,
|
|
46
|
+
identityTranslationTable,
|
|
47
|
+
initXmlWriterEx,
|
|
48
|
+
int64ToDecimalString,
|
|
49
|
+
makeTypeXsd,
|
|
50
|
+
n,
|
|
51
|
+
restoreDefaultNamespace,
|
|
52
|
+
startElementEx,
|
|
53
|
+
UAX_TYPES_XSD
|
|
54
|
+
} from "./nodeset_xml_primitives.js";
|
|
55
|
+
|
|
56
|
+
const errorLog = make_errorLog("nodeset_records_to_xml");
|
|
57
|
+
|
|
58
|
+
const UA_NAMESPACE_URI = "http://opcfoundation.org/UA/";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* the alias each id is written under, per writer. `<Aliases>` exists so that a document can say
|
|
62
|
+
* ReferenceType="HasProperty" instead of i=46, and every document that declares an alias uses it,
|
|
63
|
+
* so writing the bare id back would be a different document saying the same thing. The header
|
|
64
|
+
* carries the aliases; this is that map inverted.
|
|
65
|
+
*/
|
|
66
|
+
const aliasTables = new WeakMap<XmlWriter, Map<string, string>>();
|
|
67
|
+
|
|
68
|
+
/** an id as the document spells it: its alias when it has one, the id itself otherwise */
|
|
69
|
+
function a(xw: XmlWriter, nodeId: NodeId): string {
|
|
70
|
+
const spelling = n(xw, nodeId);
|
|
71
|
+
return aliasTables.get(xw)?.get(spelling) ?? spelling;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface RecordsToNodeset2XMLOptions {
|
|
75
|
+
/**
|
|
76
|
+
* emit the `<!--DataTypes-->` style comments the address-space exporter writes between groups
|
|
77
|
+
* of node classes. Off by default: a document's own records arrive in the document's order and
|
|
78
|
+
* there are no groups to announce.
|
|
79
|
+
*/
|
|
80
|
+
sectionComments?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** the element name of each node class, as the UANodeSet schema names it */
|
|
84
|
+
const ELEMENT_OF_NODE_CLASS: Partial<Record<NodeClass, string>> = {
|
|
85
|
+
[NodeClass.Object]: "UAObject",
|
|
86
|
+
[NodeClass.Variable]: "UAVariable",
|
|
87
|
+
[NodeClass.Method]: "UAMethod",
|
|
88
|
+
[NodeClass.ObjectType]: "UAObjectType",
|
|
89
|
+
[NodeClass.VariableType]: "UAVariableType",
|
|
90
|
+
[NodeClass.ReferenceType]: "UAReferenceType",
|
|
91
|
+
[NodeClass.DataType]: "UADataType",
|
|
92
|
+
[NodeClass.View]: "UAView"
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* a NodeSet2 document from the records it was read into.
|
|
97
|
+
*
|
|
98
|
+
* The records must be one document's worth, the header first, the nodes after it in the order the
|
|
99
|
+
* document is to list them. Ids are written with the indices the records carry.
|
|
100
|
+
*/
|
|
101
|
+
export function recordsToNodeset2XML(records: readonly NodesetRecord[], options: RecordsToNodeset2XMLOptions = {}): string {
|
|
102
|
+
const header = records[0];
|
|
103
|
+
if (header?.kind !== "header") {
|
|
104
|
+
throw new Error("recordsToNodeset2XML: the records must start with the header record");
|
|
105
|
+
}
|
|
106
|
+
// index 0 is always the UA namespace and is never listed in <NamespaceUris>; the declared uris
|
|
107
|
+
// follow it in order. This is the table the records index into and the table we write back.
|
|
108
|
+
const namespaceArray = [UA_NAMESPACE_URI, ...header.namespaceUris];
|
|
109
|
+
|
|
110
|
+
const xw: XmlWriter = new XMLWriter(true);
|
|
111
|
+
xw.translationTable = identityTranslationTable(namespaceArray.length);
|
|
112
|
+
xw.priorityTable = namespaceArray.map((_, index) => index);
|
|
113
|
+
xw.visitedNode = new Set();
|
|
114
|
+
|
|
115
|
+
xw.startDocument({ encoding: "utf-8", version: "1.0" });
|
|
116
|
+
xw.startElement("UANodeSet");
|
|
117
|
+
xw.writeAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
|
|
118
|
+
xw.writeAttribute("xmlns:uax", UAX_TYPES_XSD);
|
|
119
|
+
xw.writeAttribute("xmlns", "http://opcfoundation.org/UA/2011/03/UANodeSet.xsd");
|
|
120
|
+
|
|
121
|
+
const namespacesMap: Record<string, string> = {
|
|
122
|
+
"http://opcfoundation.org/UA/2011/03/UANodeSet.xsd": "",
|
|
123
|
+
[UAX_TYPES_XSD]: "uax",
|
|
124
|
+
"http://www.w3.org/2001/XMLSchema-instance": "xsi"
|
|
125
|
+
};
|
|
126
|
+
for (let index = 1; index < namespaceArray.length; index++) {
|
|
127
|
+
const smallName = `ns${index}`;
|
|
128
|
+
xw.writeAttribute(`xmlns:${smallName}`, makeTypeXsd(namespaceArray[index]));
|
|
129
|
+
namespacesMap[namespaceArray[index]] = smallName;
|
|
130
|
+
}
|
|
131
|
+
initXmlWriterEx(xw, namespacesMap, namespaceArray);
|
|
132
|
+
|
|
133
|
+
const aliasOf = new Map<string, string>();
|
|
134
|
+
for (const [name, nodeId] of Object.entries(header.aliases)) {
|
|
135
|
+
aliasOf.set(nodeId.toString().replace("ns=0;", ""), name);
|
|
136
|
+
}
|
|
137
|
+
aliasTables.set(xw, aliasOf);
|
|
138
|
+
|
|
139
|
+
writeHeader(xw, header);
|
|
140
|
+
|
|
141
|
+
for (const record of records.slice(1)) {
|
|
142
|
+
if (record.kind !== "node") {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
writeNode(xw, record, options);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
xw.endElement();
|
|
149
|
+
xw.endDocument();
|
|
150
|
+
return xw.toString();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* a date that can be written back. The reader keeps an unparseable PublicationDate as an invalid
|
|
155
|
+
* Date rather than dropping it, so a document that said something bad still says it was there;
|
|
156
|
+
* `toISOString` throws on one, and there is nothing to write.
|
|
157
|
+
*/
|
|
158
|
+
function isUsableDate(date: Date | undefined | null): date is Date {
|
|
159
|
+
return date instanceof Date && !Number.isNaN(date.getTime());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function writeHeader(xw: XmlWriter, header: NodesetHeaderRecord): void {
|
|
163
|
+
xw.startElement("NamespaceUris");
|
|
164
|
+
for (const namespaceUri of header.namespaceUris) {
|
|
165
|
+
xw.startElement("Uri");
|
|
166
|
+
xw.text(namespaceUri);
|
|
167
|
+
xw.endElement();
|
|
168
|
+
}
|
|
169
|
+
xw.endElement();
|
|
170
|
+
|
|
171
|
+
xw.startElement("Models");
|
|
172
|
+
for (const model of header.models) {
|
|
173
|
+
xw.startElement("Model");
|
|
174
|
+
xw.writeAttribute("ModelUri", model.modelUri);
|
|
175
|
+
xw.writeAttribute("Version", model.version);
|
|
176
|
+
if (isUsableDate(model.publicationDate)) {
|
|
177
|
+
xw.writeAttribute("PublicationDate", model.publicationDate.toISOString());
|
|
178
|
+
}
|
|
179
|
+
for (const required of model.requiredModels) {
|
|
180
|
+
xw.startElement("RequiredModel");
|
|
181
|
+
xw.writeAttribute("ModelUri", required.modelUri);
|
|
182
|
+
// both are optional in the XSD, and a document that leaves them out means it: ISA95
|
|
183
|
+
// requires the UA namespace with no version and no date at all
|
|
184
|
+
if (required.version) {
|
|
185
|
+
xw.writeAttribute("Version", required.version);
|
|
186
|
+
}
|
|
187
|
+
if (isUsableDate(required.publicationDate)) {
|
|
188
|
+
xw.writeAttribute("PublicationDate", required.publicationDate.toISOString());
|
|
189
|
+
}
|
|
190
|
+
xw.endElement();
|
|
191
|
+
}
|
|
192
|
+
xw.endElement();
|
|
193
|
+
}
|
|
194
|
+
xw.endElement();
|
|
195
|
+
|
|
196
|
+
xw.startElement("Aliases");
|
|
197
|
+
// in the order the document declared them, which the record kept. The address-space exporter
|
|
198
|
+
// sorts because it synthesizes the list and has no order to preserve; here there is one, and
|
|
199
|
+
// sorting it would make the document a rearrangement of itself rather than a copy
|
|
200
|
+
for (const name of Object.keys(header.aliases)) {
|
|
201
|
+
xw.startElement("Alias");
|
|
202
|
+
xw.writeAttribute("Alias", name);
|
|
203
|
+
xw.text(header.aliases[name].toString().replace("ns=0;", ""));
|
|
204
|
+
xw.endElement();
|
|
205
|
+
}
|
|
206
|
+
xw.endElement();
|
|
207
|
+
|
|
208
|
+
// whatever a tool stamped on the document, back as the XML it was, prefix included: the
|
|
209
|
+
// reader keeps the qualified name for the fragment cloner precisely so that this survives
|
|
210
|
+
if (header.extensions?.length) {
|
|
211
|
+
xw.startElement("Extensions");
|
|
212
|
+
for (const extension of header.extensions) {
|
|
213
|
+
xw.startElement("Extension");
|
|
214
|
+
writeRaw(xw, extension);
|
|
215
|
+
xw.endElement();
|
|
216
|
+
}
|
|
217
|
+
xw.endElement();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// #region nodes
|
|
222
|
+
|
|
223
|
+
function writeNode(xw: XmlWriter, record: NodesetNodeRecord, options: RecordsToNodeset2XMLOptions): void {
|
|
224
|
+
const element = ELEMENT_OF_NODE_CLASS[record.nodeClass];
|
|
225
|
+
if (!element) {
|
|
226
|
+
errorLog(`recordsToNodeset2XML: no element for node class ${record.nodeClass} on ${record.nodeId.toString()}`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (options.sectionComments) {
|
|
230
|
+
xw.writeComment(element.replace(/^UA/, ""));
|
|
231
|
+
}
|
|
232
|
+
xw.startElement(element);
|
|
233
|
+
writeCommonAttributes(xw, record);
|
|
234
|
+
writeClassAttributes(xw, record);
|
|
235
|
+
writeCommonElements(xw, record);
|
|
236
|
+
writeDefinition(xw, record);
|
|
237
|
+
writeValue(xw, record.value);
|
|
238
|
+
xw.endElement();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function writeCommonAttributes(xw: XmlWriter, record: NodesetNodeRecord): void {
|
|
242
|
+
xw.writeAttribute("NodeId", n(xw, record.nodeId));
|
|
243
|
+
xw.writeAttribute("BrowseName", b(xw, record.browseName));
|
|
244
|
+
if (record.parentNodeId) {
|
|
245
|
+
xw.writeAttribute("ParentNodeId", n(xw, record.parentNodeId));
|
|
246
|
+
}
|
|
247
|
+
if (record.symbolicName !== undefined) {
|
|
248
|
+
xw.writeAttribute("SymbolicName", record.symbolicName);
|
|
249
|
+
}
|
|
250
|
+
if (record.releaseStatus) {
|
|
251
|
+
xw.writeAttribute("ReleaseStatus", record.releaseStatus);
|
|
252
|
+
}
|
|
253
|
+
if (record.isAbstract) {
|
|
254
|
+
xw.writeAttribute("IsAbstract", "true");
|
|
255
|
+
}
|
|
256
|
+
if (record.accessLevel !== undefined) {
|
|
257
|
+
xw.writeAttribute("AccessLevel", record.accessLevel);
|
|
258
|
+
}
|
|
259
|
+
if (record.userAccessLevel !== undefined) {
|
|
260
|
+
xw.writeAttribute("UserAccessLevel", record.userAccessLevel);
|
|
261
|
+
}
|
|
262
|
+
if (record.accessRestrictions !== undefined) {
|
|
263
|
+
xw.writeAttribute("AccessRestrictions", record.accessRestrictions);
|
|
264
|
+
}
|
|
265
|
+
if (record.hasNoPermissions) {
|
|
266
|
+
xw.writeAttribute("HasNoPermissions", "true");
|
|
267
|
+
}
|
|
268
|
+
if (record.minimumSamplingInterval !== undefined && record.minimumSamplingInterval > 0) {
|
|
269
|
+
xw.writeAttribute("MinimumSamplingInterval", record.minimumSamplingInterval);
|
|
270
|
+
}
|
|
271
|
+
if (record.historizing) {
|
|
272
|
+
xw.writeAttribute("Historizing", "true");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function writeClassAttributes(xw: XmlWriter, record: NodesetNodeRecord): void {
|
|
277
|
+
switch (record.nodeClass) {
|
|
278
|
+
case NodeClass.Object:
|
|
279
|
+
case NodeClass.ObjectType:
|
|
280
|
+
case NodeClass.View:
|
|
281
|
+
if (record.eventNotifier) {
|
|
282
|
+
xw.writeAttribute("EventNotifier", record.eventNotifier.toString());
|
|
283
|
+
}
|
|
284
|
+
if (record.nodeClass === NodeClass.View && record.containsNoLoops) {
|
|
285
|
+
xw.writeAttribute("ContainsNoLoops", "true");
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
case NodeClass.Variable:
|
|
289
|
+
case NodeClass.VariableType:
|
|
290
|
+
if (record.dataType) {
|
|
291
|
+
xw.writeAttribute("DataType", a(xw, record.dataType));
|
|
292
|
+
}
|
|
293
|
+
if (record.valueRank !== undefined) {
|
|
294
|
+
xw.writeAttribute("ValueRank", record.valueRank.toString());
|
|
295
|
+
}
|
|
296
|
+
if (record.arrayDimensions) {
|
|
297
|
+
xw.writeAttribute("ArrayDimensions", record.arrayDimensions.join(","));
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
300
|
+
case NodeClass.Method:
|
|
301
|
+
if (record.methodDeclarationId) {
|
|
302
|
+
xw.writeAttribute("MethodDeclarationId", n(xw, record.methodDeclarationId));
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
case NodeClass.ReferenceType:
|
|
306
|
+
if (record.symmetric) {
|
|
307
|
+
xw.writeAttribute("Symmetric", "true");
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
default:
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// the UANode sequence is DisplayName, Description, Category, Documentation, References,
|
|
316
|
+
// RolePermissions: InverseName sits with DisplayName and Description, RolePermissions after
|
|
317
|
+
// References.
|
|
318
|
+
function writeCommonElements(xw: XmlWriter, record: NodesetNodeRecord): void {
|
|
319
|
+
if (record.displayName !== undefined) {
|
|
320
|
+
xw.startElement("DisplayName").text(record.displayName).endElement();
|
|
321
|
+
}
|
|
322
|
+
if (record.description !== undefined) {
|
|
323
|
+
xw.startElement("Description").text(record.description).endElement();
|
|
324
|
+
}
|
|
325
|
+
if (record.nodeClass === NodeClass.ReferenceType && record.inverseName !== undefined) {
|
|
326
|
+
xw.startElement("InverseName").text(record.inverseName).endElement();
|
|
327
|
+
}
|
|
328
|
+
for (const category of record.category ?? []) {
|
|
329
|
+
xw.startElement("Category").text(category).endElement();
|
|
330
|
+
}
|
|
331
|
+
if (record.documentation !== undefined) {
|
|
332
|
+
xw.startElement("Documentation").text(record.documentation).endElement();
|
|
333
|
+
}
|
|
334
|
+
writeReferences(xw, record.references);
|
|
335
|
+
if (record.rolePermissions && record.rolePermissions.length > 0) {
|
|
336
|
+
xw.startElement("RolePermissions");
|
|
337
|
+
for (const rolePermission of record.rolePermissions) {
|
|
338
|
+
xw.startElement("RolePermission");
|
|
339
|
+
xw.writeAttribute("Permissions", rolePermission.permissions.toString());
|
|
340
|
+
xw.text(n(xw, rolePermission.roleId));
|
|
341
|
+
xw.endElement();
|
|
342
|
+
}
|
|
343
|
+
xw.endElement();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function writeReferences(xw: XmlWriter, references: NodesetReferenceRecord[]): void {
|
|
348
|
+
if (!references || references.length === 0) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
xw.startElement("References");
|
|
352
|
+
for (const reference of references) {
|
|
353
|
+
xw.startElement("Reference");
|
|
354
|
+
xw.writeAttribute("ReferenceType", a(xw, reference.referenceType));
|
|
355
|
+
if (!reference.isForward) {
|
|
356
|
+
xw.writeAttribute("IsForward", "false");
|
|
357
|
+
}
|
|
358
|
+
xw.text(n(xw, reference.nodeId));
|
|
359
|
+
xw.endElement();
|
|
360
|
+
}
|
|
361
|
+
xw.endElement();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function writeDefinition(xw: XmlWriter, record: NodesetNodeRecord): void {
|
|
365
|
+
const definition = record.definition;
|
|
366
|
+
if (!definition) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
xw.startElement("Definition");
|
|
370
|
+
if (definition.name !== undefined) {
|
|
371
|
+
xw.writeAttribute("Name", definition.name);
|
|
372
|
+
}
|
|
373
|
+
if (definition.isUnion) {
|
|
374
|
+
xw.writeAttribute("IsUnion", "true");
|
|
375
|
+
}
|
|
376
|
+
if (definition.isOptionSet) {
|
|
377
|
+
xw.writeAttribute("IsOptionSet", "true");
|
|
378
|
+
}
|
|
379
|
+
for (const field of definition.fields || []) {
|
|
380
|
+
xw.startElement("Field");
|
|
381
|
+
xw.writeAttribute("Name", field.name || "");
|
|
382
|
+
// a field whose Name is not an identifier carries the identifier separately: "N/S
|
|
383
|
+
// Hemisphere" is declared with SymbolicName="N_S_Hemisphere", and a generator needs it
|
|
384
|
+
if (field.symbolicName) {
|
|
385
|
+
xw.writeAttribute("SymbolicName", field.symbolicName);
|
|
386
|
+
}
|
|
387
|
+
if (field.arrayDimensions && field.arrayDimensions.length > 0) {
|
|
388
|
+
xw.writeAttribute("ArrayDimensions", field.arrayDimensions.join(","));
|
|
389
|
+
}
|
|
390
|
+
if (field.valueRank !== undefined && field.valueRank !== -1) {
|
|
391
|
+
xw.writeAttribute("ValueRank", field.valueRank);
|
|
392
|
+
}
|
|
393
|
+
if (field.value !== undefined && field.value !== null) {
|
|
394
|
+
xw.writeAttribute("Value", field.value as number);
|
|
395
|
+
}
|
|
396
|
+
if (field.isOptional) {
|
|
397
|
+
xw.writeAttribute("IsOptional", "true");
|
|
398
|
+
}
|
|
399
|
+
if (field.allowSubTypes) {
|
|
400
|
+
xw.writeAttribute("AllowSubTypes", "true");
|
|
401
|
+
}
|
|
402
|
+
if (field.maxStringLength !== undefined && field.maxStringLength !== 0) {
|
|
403
|
+
xw.writeAttribute("MaxStringLength", field.maxStringLength);
|
|
404
|
+
}
|
|
405
|
+
if (field.dataType) {
|
|
406
|
+
xw.writeAttribute("DataType", a(xw, field.dataType));
|
|
407
|
+
}
|
|
408
|
+
if (field.description) {
|
|
409
|
+
const text = typeof field.description === "string" ? field.description : (field.description as { text?: string }).text;
|
|
410
|
+
if (text) {
|
|
411
|
+
xw.startElement("Description").text(text).endElement();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
xw.endElement();
|
|
415
|
+
}
|
|
416
|
+
xw.endElement();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// #endregion
|
|
420
|
+
|
|
421
|
+
// #region values
|
|
422
|
+
|
|
423
|
+
function writeValue(xw: XmlWriter, variant: VariantOptions | undefined): void {
|
|
424
|
+
if (!variant || variant.value === null || variant.value === undefined) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const dataType = variant.dataType as DataType;
|
|
428
|
+
if (dataType === undefined || dataType === DataType.Null) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const builtInName = DataType[dataType];
|
|
432
|
+
xw.startElement("Value");
|
|
433
|
+
const uax = getPrefix(xw, UAX_TYPES_XSD);
|
|
434
|
+
const isArray = variant.arrayType === VariantArrayType.Array || variant.arrayType === VariantArrayType.Matrix;
|
|
435
|
+
if (isArray) {
|
|
436
|
+
// the built-in type, never a nodeset's own DataType name: uax defines ListOfInt32, and a
|
|
437
|
+
// reader that meets a ListOfSomethingElse walks past it into the first child
|
|
438
|
+
startElementEx(xw, uax, `ListOf${builtInName}`, UAX_TYPES_XSD);
|
|
439
|
+
for (const element of variant.value as unknown[]) {
|
|
440
|
+
writeOneValue(xw, dataType, element, builtInName);
|
|
441
|
+
}
|
|
442
|
+
restoreDefaultNamespace(xw);
|
|
443
|
+
xw.endElement();
|
|
444
|
+
} else {
|
|
445
|
+
writeOneValue(xw, dataType, variant.value, builtInName);
|
|
446
|
+
}
|
|
447
|
+
xw.endElement();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** one value of the built-in type, wrapped in its own `uax:` element unless it is an extension object */
|
|
451
|
+
function writeOneValue(xw: XmlWriter, dataType: DataType, value: unknown, builtInName: string): void {
|
|
452
|
+
if (dataType === DataType.ExtensionObject) {
|
|
453
|
+
writeExtensionObject(xw, value as ExtensionObject | XmlExtensionObjectFragment);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const uax = getPrefix(xw, UAX_TYPES_XSD);
|
|
457
|
+
xw.startElement(`${uax}${builtInName}`);
|
|
458
|
+
writeScalar(xw, dataType, value);
|
|
459
|
+
xw.endElement();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function writeScalar(xw: XmlWriter, dataType: DataType, value: unknown): void {
|
|
463
|
+
if (value === null || value === undefined) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const uax = getPrefix(xw, UAX_TYPES_XSD);
|
|
467
|
+
switch (dataType) {
|
|
468
|
+
case DataType.LocalizedText:
|
|
469
|
+
_dumpLocalizedText(xw, value as Parameters<typeof _dumpLocalizedText>[1]);
|
|
470
|
+
break;
|
|
471
|
+
case DataType.QualifiedName:
|
|
472
|
+
_dumpQualifiedName(xw, value as Parameters<typeof _dumpQualifiedName>[1]);
|
|
473
|
+
break;
|
|
474
|
+
case DataType.NodeId:
|
|
475
|
+
case DataType.ExpandedNodeId:
|
|
476
|
+
_dumpNodeId(xw, value as NodeId);
|
|
477
|
+
break;
|
|
478
|
+
case DataType.DateTime:
|
|
479
|
+
xw.text((value as Date).toISOString());
|
|
480
|
+
break;
|
|
481
|
+
case DataType.Int64:
|
|
482
|
+
case DataType.UInt64:
|
|
483
|
+
xw.text(int64ToDecimalString(value as Int64, dataType === DataType.Int64));
|
|
484
|
+
break;
|
|
485
|
+
case DataType.ByteString: {
|
|
486
|
+
const base64 = Buffer.from(value as Uint8Array).toString("base64");
|
|
487
|
+
const chunks = base64.match(/.{0,80}/g);
|
|
488
|
+
xw.text(base64.length > 80 && chunks ? chunks.join("\n") : base64);
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
case DataType.Guid:
|
|
492
|
+
xw.startElement(`${uax}String`).text(String(value)).endElement();
|
|
493
|
+
break;
|
|
494
|
+
case DataType.StatusCode:
|
|
495
|
+
xw.text(String((value as { value: number }).value));
|
|
496
|
+
break;
|
|
497
|
+
case DataType.XmlElement:
|
|
498
|
+
writeRaw(xw, String(value));
|
|
499
|
+
break;
|
|
500
|
+
default:
|
|
501
|
+
// every remaining built-in is a scalar the XSD writes as text
|
|
502
|
+
xw.text(String(value));
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* an extension object: the XML the reader kept, or one of the four types it decodes. There is no
|
|
509
|
+
* fifth case -- the image codec will not carry a decoded anything else -- so no DataType definition
|
|
510
|
+
* is ever needed to write one back.
|
|
511
|
+
*/
|
|
512
|
+
function writeExtensionObject(xw: XmlWriter, value: ExtensionObject | XmlExtensionObjectFragment): void {
|
|
513
|
+
const uax = getPrefix(xw, UAX_TYPES_XSD);
|
|
514
|
+
if (value instanceof XmlExtensionObjectFragment) {
|
|
515
|
+
startElementEx(xw, uax, "ExtensionObject", UAX_TYPES_XSD);
|
|
516
|
+
writeTypeId(xw, value.typeId);
|
|
517
|
+
startElementEx(xw, uax, "Body", UAX_TYPES_XSD);
|
|
518
|
+
writeRaw(xw, value.bodyXML);
|
|
519
|
+
restoreDefaultNamespace(xw);
|
|
520
|
+
xw.endElement();
|
|
521
|
+
restoreDefaultNamespace(xw);
|
|
522
|
+
xw.endElement();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const encodingId = value.schema?.encodingDefaultXml;
|
|
526
|
+
if (!encodingId || encodingId.isEmpty()) {
|
|
527
|
+
errorLog(`recordsToNodeset2XML: ${value.schema?.name ?? "extension object"} has no XML encoding`);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
startElementEx(xw, uax, "ExtensionObject", UAX_TYPES_XSD);
|
|
531
|
+
writeTypeId(xw, encodingId);
|
|
532
|
+
startElementEx(xw, uax, "Body", UAX_TYPES_XSD);
|
|
533
|
+
writeDecodedExtensionObject(xw, value);
|
|
534
|
+
restoreDefaultNamespace(xw);
|
|
535
|
+
xw.endElement();
|
|
536
|
+
restoreDefaultNamespace(xw);
|
|
537
|
+
xw.endElement();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function writeTypeId(xw: XmlWriter, typeId: NodeId): void {
|
|
541
|
+
const uax = getPrefix(xw, UAX_TYPES_XSD);
|
|
542
|
+
xw.startElement(`${uax}TypeId`);
|
|
543
|
+
xw.startElement(`${uax}Identifier`);
|
|
544
|
+
xw.text(n(xw, typeId));
|
|
545
|
+
xw.endElement();
|
|
546
|
+
xw.endElement();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function writeDecodedExtensionObject(xw: XmlWriter, value: ExtensionObject): void {
|
|
550
|
+
const uax = getPrefix(xw, UAX_TYPES_XSD);
|
|
551
|
+
const element = (name: string, write: () => void) => {
|
|
552
|
+
xw.startElement(`${uax}${name}`);
|
|
553
|
+
write();
|
|
554
|
+
xw.endElement();
|
|
555
|
+
};
|
|
556
|
+
const text = (name: string, content: string) => element(name, () => xw.text(content));
|
|
557
|
+
const localizedText = (name: string, v: { locale?: string | null; text?: string | null } | null | undefined) => {
|
|
558
|
+
// null, not undefined: a LocalizedText that carried nothing comes back from the codec with
|
|
559
|
+
// null fields rather than absent ones, and testing for undefined wrote an empty
|
|
560
|
+
// <Description> onto every Argument that never had one
|
|
561
|
+
if (!v || (!v.text && !v.locale)) return;
|
|
562
|
+
element(name, () => _dumpLocalizedText(xw, v as Parameters<typeof _dumpLocalizedText>[1]));
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
if (value instanceof Argument) {
|
|
566
|
+
xw.startElement(`${uax}Argument`);
|
|
567
|
+
text("Name", value.name ?? "");
|
|
568
|
+
element("DataType", () => _dumpNodeId(xw, value.dataType));
|
|
569
|
+
text("ValueRank", String(value.valueRank));
|
|
570
|
+
// an empty ArrayDimensions is an empty element, which is what a scalar argument declares
|
|
571
|
+
element("ArrayDimensions", () => {
|
|
572
|
+
for (const dimension of value.arrayDimensions ?? []) {
|
|
573
|
+
text("UInt32", String(dimension));
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
localizedText("Description", value.description);
|
|
577
|
+
xw.endElement();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (value instanceof EUInformation) {
|
|
581
|
+
xw.startElement(`${uax}EUInformation`);
|
|
582
|
+
text("NamespaceUri", value.namespaceUri ?? "");
|
|
583
|
+
text("UnitId", String(value.unitId));
|
|
584
|
+
localizedText("DisplayName", value.displayName);
|
|
585
|
+
localizedText("Description", value.description);
|
|
586
|
+
xw.endElement();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (value instanceof Range) {
|
|
590
|
+
xw.startElement(`${uax}Range`);
|
|
591
|
+
text("Low", String(value.low));
|
|
592
|
+
text("High", String(value.high));
|
|
593
|
+
xw.endElement();
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (value instanceof EnumValueType) {
|
|
597
|
+
xw.startElement(`${uax}EnumValueType`);
|
|
598
|
+
text("Value", Array.isArray(value.value) ? String(value.value[1]) : String(value.value));
|
|
599
|
+
localizedText("DisplayName", value.displayName);
|
|
600
|
+
localizedText("Description", value.description);
|
|
601
|
+
xw.endElement();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
errorLog(
|
|
605
|
+
`recordsToNodeset2XML: a record cannot hold a decoded ${value.constructor?.name ?? "extension object"}; ` +
|
|
606
|
+
"the reader keeps anything but the four known types as its XML"
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** raw XML, unescaped: a fragment the reader kept is already a document, not text to be quoted */
|
|
611
|
+
function writeRaw(xw: XmlWriter, content: string): void {
|
|
612
|
+
const raw = (xw as unknown as { writeRaw?: (s: string) => void }).writeRaw;
|
|
613
|
+
if (typeof raw === "function") {
|
|
614
|
+
raw.call(xw, content);
|
|
615
|
+
} else {
|
|
616
|
+
xw.text(content);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// #endregion
|