Skip to main content

opcua_nodes/
xml.rs

1use std::{
2    path::Path,
3    str::FromStr,
4    sync::{Arc, OnceLock},
5};
6
7use hashbrown::HashMap;
8use opcua_types::{
9    Context, DataTypeDefinition, DataValue, DecodingOptions, EnumDefinition, EnumField, Error,
10    LocalizedText, NodeClass, NodeId, QualifiedName, StructureDefinition, StructureField,
11    StructureType, TypeLoader, TypeLoaderCollection, Variant,
12};
13use opcua_xml::{
14    load_nodeset2_file,
15    schema::ua_node_set::{
16        self, ArrayDimensions, ListOfReferences, UADataType, UAMethod, UANodeSet, UAObject,
17        UAObjectType, UAReferenceType, UAVariable, UAVariableType, UAView,
18    },
19    XmlError,
20};
21use regex::Regex;
22use tracing::warn;
23
24use crate::{
25    Base, DataType, EventNotifier, ImportedItem, ImportedReference, Method, NodeSetImport, Object,
26    ObjectType, ReferenceType, Variable, VariableType, View,
27};
28
29/// [`NodeSetImport`] implementation for dynamically loading NodeSet2 files at
30/// runtime. Note that structures must be loaded with a type loader. By default
31/// the type loader for the base types is registered, but if your NodeSet2 file uses custom types
32/// you will have to add an [`TypeLoader`] using [`NodeSet2Import::add_type_loader`].
33pub struct NodeSet2Import {
34    type_loaders: TypeLoaderCollection,
35    dependent_namespaces: Vec<String>,
36    preferred_locale: String,
37    aliases: HashMap<String, String>,
38    file: UANodeSet,
39}
40
41static QUALIFIED_NAME_REGEX: OnceLock<Regex> = OnceLock::new();
42
43fn qualified_name_regex() -> &'static Regex {
44    QUALIFIED_NAME_REGEX.get_or_init(|| Regex::new(r"^((?P<ns>[0-9]+):)?(?P<name>.*)$").unwrap())
45}
46
47#[derive(thiserror::Error, Debug)]
48/// Error when loading NodeSet2 XML.
49pub enum LoadXmlError {
50    /// The XML file failed to parse.
51    #[error("{0}")]
52    Xml(#[from] XmlError),
53    /// The file failed to load.
54    #[error("{0}")]
55    Io(#[from] std::io::Error),
56    /// The nodeset section is missing from the file. It is most likely invalid.
57    #[error("Missing <NodeSet> section from file")]
58    MissingNodeSet,
59}
60
61impl NodeSet2Import {
62    /// Create a new NodeSet2 importer.
63    /// The `dependent_namespaces` array contains namespaces that this nodeset requires, in order,
64    /// but that are _not_ included in the nodeset file itself.
65    /// It does not need to include the base namespace, but it may.
66    ///
67    /// # Example
68    ///
69    /// ```ignore
70    /// NodeSet2Import::new(
71    ///     "en",
72    ///     "My.ISA95.Extension.NodeSet2.xml",
73    ///     // Since we depend on ISA95, we need to include the ISA95 namespace.
74    ///     // Typically, the NodeSet will reference ns=1 as ISA95, and ns=2 as its own
75    ///     // namespace, this will allow us to interpret ns=1 correctly. Without this,
76    ///     // we would panic when failing to look up ns=2.
77    ///     vec!["http://www.OPCFoundation.org/UA/2013/01/ISA95"]
78    /// )
79    /// ```
80    pub fn new(
81        preferred_locale: &str,
82        path: impl AsRef<Path>,
83        dependent_namespaces: Vec<String>,
84    ) -> Result<Self, LoadXmlError> {
85        let content = std::fs::read_to_string(path)?;
86        Self::new_str(preferred_locale, &content, dependent_namespaces)
87    }
88
89    /// Create a new NodeSet2 importer from an already loaded `NodeSet2.xml` file.
90    ///
91    /// See documentation of [NodeSet2Import::new].
92    pub fn new_str(
93        preferred_locale: &str,
94        nodeset: &str,
95        dependent_namespaces: Vec<String>,
96    ) -> Result<Self, LoadXmlError> {
97        let nodeset = load_nodeset2_file(nodeset)?;
98        let nodeset = nodeset.node_set.ok_or(LoadXmlError::MissingNodeSet)?;
99
100        Ok(Self::new_nodeset(
101            preferred_locale,
102            nodeset,
103            dependent_namespaces,
104        ))
105    }
106
107    /// Create a new importer with a pre-loaded nodeset.
108    /// The `dependent_namespaces` array contains namespaces that this nodeset requires, in order,
109    /// but that are _not_ included in the nodeset file itself.
110    /// It does not need to include the base namespace, but it may.
111    pub fn new_nodeset(
112        preferred_locale: &str,
113        nodeset: UANodeSet,
114        dependent_namespaces: Vec<String>,
115    ) -> Self {
116        let aliases = nodeset
117            .aliases
118            .iter()
119            .flat_map(|i| i.aliases.iter())
120            .map(|alias| (alias.alias.clone(), alias.id.0.clone()))
121            .collect();
122        Self {
123            preferred_locale: preferred_locale.to_owned(),
124            type_loaders: TypeLoaderCollection::new(),
125            file: nodeset,
126            dependent_namespaces,
127            aliases,
128        }
129    }
130
131    /// Add a type loader for importing types from XML.
132    ///
133    /// Any custom variable Value must be supported by one of the added
134    /// type loaders in order for the node set import to work.
135    pub fn add_type_loader(&mut self, loader: Arc<dyn TypeLoader>) {
136        self.type_loaders.add(loader);
137    }
138
139    fn select_localized_text(&self, texts: &[ua_node_set::LocalizedText]) -> Option<LocalizedText> {
140        let mut selected_str = None;
141        for text in texts {
142            if text.locale.0.is_empty() && selected_str.is_none()
143                || text.locale.0 == self.preferred_locale
144            {
145                selected_str = Some(text);
146            }
147        }
148        let selected_str = selected_str.or_else(|| texts.first());
149        let selected = selected_str?;
150        Some(LocalizedText::new(&selected.locale.0, &selected.text))
151    }
152
153    fn make_node_id(
154        &self,
155        node_id: &ua_node_set::NodeId,
156        ctx: &Context<'_>,
157    ) -> Result<NodeId, Error> {
158        let node_id_str = ctx.resolve_alias(&node_id.0);
159
160        let Some(mut parsed) = NodeId::from_str(node_id_str).ok() else {
161            return Err(Error::decoding(format!(
162                "Failed to parse node ID: {node_id_str}"
163            )));
164        };
165
166        parsed.namespace = ctx.resolve_namespace_index(parsed.namespace)?;
167        Ok(parsed)
168    }
169
170    fn make_qualified_name(
171        &self,
172        qname: &ua_node_set::QualifiedName,
173        ctx: &Context<'_>,
174    ) -> Result<QualifiedName, Error> {
175        let captures = qualified_name_regex()
176            .captures(&qname.0)
177            .ok_or_else(|| Error::decoding(format!("Invalid qualified name: {}", qname.0)))?;
178
179        let namespace = if let Some(ns) = captures.name("ns") {
180            ns.as_str().trim().parse::<u16>().map_err(|e| {
181                Error::decoding(format!(
182                    "Failed to parse namespace index from qualified name: {}, {e:?}",
183                    qname.0
184                ))
185            })?
186        } else {
187            0
188        };
189
190        let namespace = ctx.resolve_namespace_index(namespace)?;
191        let name = captures.name("name").map(|n| n.as_str()).unwrap_or("");
192        Ok(QualifiedName::new(namespace, name))
193    }
194
195    fn make_array_dimensions(&self, dims: &ArrayDimensions) -> Result<Option<Vec<u32>>, Error> {
196        if dims.0.trim().is_empty() {
197            return Ok(None);
198        }
199
200        let mut values = Vec::new();
201        for it in dims.0.split(',') {
202            let Ok(r) = it.trim().parse::<u32>() else {
203                return Err(Error::decoding(format!(
204                    "Invalid array dimensions: {}",
205                    dims.0
206                )));
207            };
208            values.push(r);
209        }
210        if values.is_empty() {
211            Ok(None)
212        } else {
213            Ok(Some(values))
214        }
215    }
216
217    fn make_data_type_def(
218        &self,
219        def: &ua_node_set::DataTypeDefinition,
220        ctx: &Context<'_>,
221    ) -> Result<DataTypeDefinition, Error> {
222        let is_enum = def.fields.first().is_some_and(|f| f.value != -1);
223        if is_enum {
224            let fields = def
225                .fields
226                .iter()
227                .map(|field| EnumField {
228                    value: field.value,
229                    display_name: self
230                        .select_localized_text(&field.display_names)
231                        .unwrap_or_default(),
232                    description: self
233                        .select_localized_text(&field.descriptions)
234                        .unwrap_or_default(),
235                    name: field.name.clone().into(),
236                })
237                .collect();
238            Ok(DataTypeDefinition::Enum(EnumDefinition {
239                fields: Some(fields),
240            }))
241        } else {
242            let mut any_optional = false;
243            let mut fields = Vec::with_capacity(def.fields.len());
244            for field in &def.fields {
245                any_optional |= field.is_optional;
246                fields.push(StructureField {
247                    name: field.name.clone().into(),
248                    description: self
249                        .select_localized_text(&field.descriptions)
250                        .unwrap_or_default(),
251                    data_type: self.make_node_id(&field.data_type, ctx).unwrap_or_default(),
252                    value_rank: field.value_rank.0,
253                    array_dimensions: self.make_array_dimensions(&field.array_dimensions)?,
254                    max_string_length: field.max_string_length as u32,
255                    is_optional: field.is_optional,
256                });
257            }
258            Ok(DataTypeDefinition::Structure(StructureDefinition {
259                default_encoding_id: NodeId::null(),
260                base_data_type: NodeId::null(),
261                structure_type: if def.is_union {
262                    StructureType::Union
263                } else if any_optional {
264                    StructureType::StructureWithOptionalFields
265                } else {
266                    StructureType::Structure
267                },
268                fields: Some(fields),
269            }))
270        }
271    }
272
273    fn make_base(
274        &self,
275        ctx: &Context<'_>,
276        base: &ua_node_set::UANodeBase,
277        node_class: NodeClass,
278    ) -> Result<Base, Error> {
279        Ok(Base::new_full(
280            self.make_node_id(&base.node_id, ctx)?,
281            node_class,
282            self.make_qualified_name(&base.browse_name, ctx)?,
283            self.select_localized_text(&base.display_names)
284                .unwrap_or_default(),
285            self.select_localized_text(&base.description),
286            Some(base.write_mask.0),
287            Some(base.user_write_mask.0),
288        ))
289    }
290
291    fn make_references(
292        &self,
293        ctx: &Context<'_>,
294        base: &Base,
295        refs: &Option<ListOfReferences>,
296    ) -> Result<Vec<ImportedReference>, Error> {
297        let Some(refs) = refs.as_ref() else {
298            return Ok(Vec::new());
299        };
300        let mut res = Vec::with_capacity(refs.references.len());
301        for rf in &refs.references {
302            let target_id = self.make_node_id(&rf.node_id, ctx).inspect_err(|e| {
303                warn!(
304                    "Invalid target ID {} on reference from node {}: {e}",
305                    rf.node_id.0, base.node_id
306                )
307            })?;
308
309            let type_id = self
310                .make_node_id(&rf.reference_type, ctx)
311                .inspect_err(|e| {
312                    warn!(
313                        "Invalid reference type ID {} on reference from node {}: {e}",
314                        rf.node_id.0, base.node_id
315                    )
316                })?;
317            res.push(ImportedReference {
318                target_id,
319                type_id,
320                is_forward: rf.is_forward,
321            });
322        }
323        Ok(res)
324    }
325
326    fn make_object(&self, ctx: &Context<'_>, node: &UAObject) -> Result<ImportedItem, Error> {
327        let base = self.make_base(ctx, &node.base.base, NodeClass::Object)?;
328        Ok(ImportedItem {
329            references: self.make_references(ctx, &base, &node.base.base.references)?,
330            node: Object::new_full(
331                base,
332                EventNotifier::from_bits_truncate(node.event_notifier.0),
333            )
334            .into(),
335        })
336    }
337
338    fn make_variable(&self, ctx: &Context<'_>, node: &UAVariable) -> Result<ImportedItem, Error> {
339        let base = self.make_base(ctx, &node.base.base, NodeClass::Variable)?;
340        Ok(ImportedItem {
341            references: self.make_references(ctx, &base, &node.base.base.references)?,
342            node: Variable::new_full(
343                base,
344                self.make_node_id(&node.data_type, ctx)?,
345                node.historizing,
346                node.value_rank.0,
347                node.value
348                    .as_ref()
349                    .map(|v| {
350                        Ok::<DataValue, Error>(DataValue::new_now(Variant::from_nodeset(
351                            &v.0, ctx,
352                        )?))
353                    })
354                    .transpose()?
355                    .unwrap_or_else(DataValue::null),
356                node.access_level.0,
357                node.user_access_level.0,
358                self.make_array_dimensions(&node.array_dimensions)?,
359                Some(node.minimum_sampling_interval.0),
360            )
361            .into(),
362        })
363    }
364
365    fn make_method(&self, ctx: &Context<'_>, node: &UAMethod) -> Result<ImportedItem, Error> {
366        let base = self.make_base(ctx, &node.base.base, NodeClass::Method)?;
367        Ok(ImportedItem {
368            references: self.make_references(ctx, &base, &node.base.base.references)?,
369            node: Method::new_full(base, node.executable, node.user_executable).into(),
370        })
371    }
372
373    fn make_view(&self, ctx: &Context<'_>, node: &UAView) -> Result<ImportedItem, Error> {
374        let base = self.make_base(ctx, &node.base.base, NodeClass::View)?;
375        Ok(ImportedItem {
376            references: self.make_references(ctx, &base, &node.base.base.references)?,
377            node: View::new_full(
378                base,
379                EventNotifier::from_bits_truncate(node.event_notifier.0),
380                node.contains_no_loops,
381            )
382            .into(),
383        })
384    }
385
386    fn make_object_type(
387        &self,
388        ctx: &Context<'_>,
389        node: &UAObjectType,
390    ) -> Result<ImportedItem, Error> {
391        let base = self.make_base(ctx, &node.base.base, NodeClass::ObjectType)?;
392        Ok(ImportedItem {
393            references: self.make_references(ctx, &base, &node.base.base.references)?,
394            node: ObjectType::new_full(base, node.base.is_abstract).into(),
395        })
396    }
397
398    fn make_variable_type(
399        &self,
400        ctx: &Context<'_>,
401        node: &UAVariableType,
402    ) -> Result<ImportedItem, Error> {
403        let base = self.make_base(ctx, &node.base.base, NodeClass::VariableType)?;
404        Ok(ImportedItem {
405            references: self.make_references(ctx, &base, &node.base.base.references)?,
406            node: VariableType::new_full(
407                base,
408                self.make_node_id(&node.data_type, ctx)?,
409                node.base.is_abstract,
410                node.value_rank.0,
411                node.value
412                    .as_ref()
413                    .map(|v| Ok::<_, Error>(DataValue::new_now(Variant::from_nodeset(&v.0, ctx)?)))
414                    .transpose()?,
415                self.make_array_dimensions(&node.array_dimensions)?,
416            )
417            .into(),
418        })
419    }
420
421    fn make_data_type(&self, ctx: &Context<'_>, node: &UADataType) -> Result<ImportedItem, Error> {
422        let base = self.make_base(ctx, &node.base.base, NodeClass::DataType)?;
423        Ok(ImportedItem {
424            references: self.make_references(ctx, &base, &node.base.base.references)?,
425            node: DataType::new_full(
426                base,
427                node.base.is_abstract,
428                node.definition
429                    .as_ref()
430                    .map(|v| self.make_data_type_def(v, ctx))
431                    .transpose()?,
432            )
433            .into(),
434        })
435    }
436
437    fn make_reference_type(
438        &self,
439        ctx: &Context<'_>,
440        node: &UAReferenceType,
441    ) -> Result<ImportedItem, Error> {
442        let base = self.make_base(ctx, &node.base.base, NodeClass::ReferenceType)?;
443        Ok(ImportedItem {
444            references: self.make_references(ctx, &base, &node.base.base.references)?,
445            node: ReferenceType::new_full(
446                base,
447                node.symmetric,
448                node.base.is_abstract,
449                self.select_localized_text(&node.inverse_names),
450            )
451            .into(),
452        })
453    }
454}
455
456impl NodeSetImport for NodeSet2Import {
457    fn register_namespaces(&self, namespaces: &mut opcua_types::NodeSetNamespaceMapper) {
458        let nss = self.get_own_namespaces();
459        // If the root namespace is in the namespace array, use absolute indexes,
460        // else, start at 1
461        let mut offset = 1;
462        for (idx, ns) in self
463            .dependent_namespaces
464            .iter()
465            .chain(nss.iter())
466            .enumerate()
467        {
468            if ns == "http://opcfoundation.org/UA/" {
469                offset = 0;
470                continue;
471            }
472            println!("Adding new namespace: {idx} {ns}");
473            namespaces.add_namespace(ns, idx as u16 + offset);
474        }
475    }
476
477    fn get_own_namespaces(&self) -> Vec<String> {
478        self.file
479            .namespace_uris
480            .as_ref()
481            .map(|n| n.uris.clone())
482            .unwrap_or_default()
483    }
484
485    fn load<'a>(
486        &'a self,
487        namespaces: &'a opcua_types::NodeSetNamespaceMapper,
488    ) -> Box<dyn Iterator<Item = crate::ImportedItem> + 'a> {
489        let mut ctx = Context::new(
490            namespaces.namespaces(),
491            &self.type_loaders,
492            DecodingOptions::default(),
493        );
494        ctx.set_aliases(&self.aliases);
495        Box::new(self.file.nodes.iter().filter_map(move |raw_node| {
496            let r = match raw_node {
497                opcua_xml::schema::ua_node_set::UANode::Object(node) => {
498                    self.make_object(&ctx, node)
499                }
500                opcua_xml::schema::ua_node_set::UANode::Variable(node) => {
501                    self.make_variable(&ctx, node)
502                }
503                opcua_xml::schema::ua_node_set::UANode::Method(node) => {
504                    self.make_method(&ctx, node)
505                }
506                opcua_xml::schema::ua_node_set::UANode::View(node) => self.make_view(&ctx, node),
507                opcua_xml::schema::ua_node_set::UANode::ObjectType(node) => {
508                    self.make_object_type(&ctx, node)
509                }
510                opcua_xml::schema::ua_node_set::UANode::VariableType(node) => {
511                    self.make_variable_type(&ctx, node)
512                }
513                opcua_xml::schema::ua_node_set::UANode::DataType(node) => {
514                    self.make_data_type(&ctx, node)
515                }
516                opcua_xml::schema::ua_node_set::UANode::ReferenceType(node) => {
517                    self.make_reference_type(&ctx, node)
518                }
519            };
520            match r {
521                Ok(r) => Some(r),
522                Err(e) => {
523                    println!("Failed to import node {}: {e}", raw_node.base().node_id.0);
524                    None
525                }
526            }
527        }))
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use opcua_types::{
534        DataTypeId, EUInformation, ExtensionObject, LocalizedText, NamespaceMap,
535        NodeSetNamespaceMapper, QualifiedName, Variant,
536    };
537
538    use crate::{NodeBase, NodeSetImport, NodeType};
539
540    use super::NodeSet2Import;
541
542    const TEST_NODESET: &str = r#"
543<UANodeSet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" LastModified="2023-12-15T00:00:00Z" xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">
544  <NamespaceUris>
545    <Uri>http://test.com</Uri>
546  </NamespaceUris>
547  <Models>
548    <Model ModelUri="http://test.com" Version="1.00" PublicationDate="2013-11-06T00:00:00Z">
549      <RequiredModel ModelUri="http://opcfoundation.org/UA/" />
550    </Model>
551  </Models>
552  <Aliases>
553    <Alias Alias="Int32">i=6</Alias>
554    <Alias Alias="HasComponent">i=47</Alias>
555    <Alias Alias="HasSubtype">i=45</Alias>
556  </Aliases>
557  <UAObject NodeId="ns=1;i=1" BrowseName="1:My Root">
558    <DisplayName>My Root</DisplayName>
559    <Description>My description</Description>
560    <References>
561      <Reference ReferenceType="HasComponent" IsForward="false">i=85</Reference>
562      <Reference ReferenceType="i=40">i=61</Reference>
563    </References>
564  </UAObject>
565  <UAVariable NodeId="ns=1;i=2" BrowseName="1:My Property" DataType="i=887">
566    <DisplayName>My Property</DisplayName>
567    <Description>My description</Description>
568    <References>
569      <Reference ReferenceType="i=40">i=68</Reference>
570      <Reference ReferenceType="i=46" IsForward="false">ns=1;i=1</Reference>
571    </References>
572    <Value>
573      <ExtensionObject>
574        <TypeId><Identifier>i=888</Identifier></TypeId>
575        <Body>
576          <EUInformation>
577            <NamespaceUri>http://unit-namespace.namespace</NamespaceUri>
578            <UnitId>15</UnitId>
579            <DisplayName>
580                <Locale>en</Locale>
581                <Text>Degrees Celsius</Text>
582            </DisplayName>
583          </EUInformation>
584        </Body>
585      </ExtensionObject>
586    </Value>
587  </UAVariable>
588</UANodeSet>"#;
589
590    #[test]
591    fn test_load_xml_nodeset() {
592        let import = NodeSet2Import::new_str("en", TEST_NODESET, vec![]).unwrap();
593        assert_eq!(
594            import.get_own_namespaces(),
595            vec!["http://test.com".to_owned()]
596        );
597        let mut ns = NamespaceMap::new();
598        let mut map = NodeSetNamespaceMapper::new(&mut ns);
599        import.register_namespaces(&mut map);
600        let nodes: Vec<_> = import.load(&map).collect();
601        assert_eq!(nodes.len(), 2);
602        let node = &nodes[0];
603        let NodeType::Object(o) = &node.node else {
604            panic!("Unexpected node type");
605        };
606        assert_eq!(o.display_name(), &LocalizedText::new("", "My Root"));
607        assert_eq!(o.browse_name(), &QualifiedName::new(1, "My Root"));
608        assert_eq!(node.references.len(), 2);
609
610        let node = &nodes[1];
611        let NodeType::Variable(v) = &node.node else {
612            panic!("Unexpected node type");
613        };
614        assert_eq!(v.display_name(), &LocalizedText::new("", "My Property"));
615        assert_eq!(v.browse_name(), &QualifiedName::new(1, "My Property"));
616        assert_eq!(v.data_type(), DataTypeId::EUInformation);
617        assert_eq!(
618            v.value.value,
619            Some(Variant::ExtensionObject(ExtensionObject::from_message(
620                EUInformation {
621                    namespace_uri: "http://unit-namespace.namespace".into(),
622                    unit_id: 15,
623                    display_name: LocalizedText::new("en", "Degrees Celsius"),
624                    description: LocalizedText::null()
625                }
626            )))
627        );
628    }
629}