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
29pub 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)]
48pub enum LoadXmlError {
50 #[error("{0}")]
52 Xml(#[from] XmlError),
53 #[error("{0}")]
55 Io(#[from] std::io::Error),
56 #[error("Missing <NodeSet> section from file")]
58 MissingNodeSet,
59}
60
61impl NodeSet2Import {
62 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 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 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 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 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}