nodeset.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. ### This Source Code Form is subject to the terms of the Mozilla Public
  4. ### License, v. 2.0. If a copy of the MPL was not distributed with this
  5. ### file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6. ### Copyright 2014-2015 (c) TU-Dresden (Author: Chris Iatrou)
  7. ### Copyright 2014-2017 (c) Fraunhofer IOSB (Author: Julius Pfrommer)
  8. ### Copyright 2016-2017 (c) Stefan Profanter, fortiss GmbH
  9. from __future__ import print_function
  10. import sys
  11. import xml.dom.minidom as dom
  12. import logging
  13. import codecs
  14. import re
  15. from datatypes import NodeId, valueIsInternalType
  16. from nodes import *
  17. from opaque_type_mapping import opaque_type_mapping
  18. __all__ = ['NodeSet', 'getSubTypesOf']
  19. logger = logging.getLogger(__name__)
  20. if sys.version_info[0] >= 3:
  21. # strings are already parsed to unicode
  22. def unicode(s):
  23. return s
  24. string_types = str
  25. else:
  26. string_types = basestring
  27. ####################
  28. # Helper Functions #
  29. ####################
  30. hassubtype = NodeId("ns=0;i=45")
  31. def getSubTypesOf(nodeset, node, skipNodes=[]):
  32. if node in skipNodes:
  33. return []
  34. re = set()
  35. re.add(node)
  36. for ref in node.references:
  37. if (ref.referenceType == hassubtype):
  38. skipAll = set()
  39. skipAll.update(skipNodes)
  40. skipAll.update(re)
  41. if (ref.source == node.id and ref.isForward):
  42. re.update(getSubTypesOf(nodeset, nodeset.nodes[ref.target], skipNodes=skipAll))
  43. elif (ref.target == node.id and not ref.isForward):
  44. re.update(getSubTypesOf(nodeset, nodeset.nodes[ref.source], skipNodes=skipAll))
  45. return re
  46. def extractNamespaces(xmlfile):
  47. # Extract a list of namespaces used. The first namespace is always
  48. # "http://opcfoundation.org/UA/". minidom gobbles up
  49. # <NamespaceUris></NamespaceUris> elements, without a decent way to reliably
  50. # access this dom2 <uri></uri> elements (only attribute xmlns= are accessible
  51. # using minidom). We need them for dereferencing though... This function
  52. # attempts to do just that.
  53. namespaces = ["http://opcfoundation.org/UA/"]
  54. infile = codecs.open(xmlfile.name, encoding='utf-8')
  55. foundURIs = False
  56. nsline = ""
  57. for line in infile:
  58. if "<namespaceuris>" in line.lower():
  59. foundURIs = True
  60. elif "</namespaceuris>" in line.lower():
  61. nsline = nsline + line
  62. break
  63. if foundURIs:
  64. nsline = nsline + line
  65. if len(nsline) > 0:
  66. ns = dom.parseString(nsline).getElementsByTagName("NamespaceUris")
  67. for uri in ns[0].childNodes:
  68. if uri.nodeType != uri.ELEMENT_NODE:
  69. continue
  70. if uri.firstChild.data in namespaces:
  71. continue
  72. namespaces.append(uri.firstChild.data)
  73. infile.close()
  74. return namespaces
  75. def buildAliasList(xmlelement):
  76. """Parses the <Alias> XML Element present in must XML NodeSet definitions.
  77. Contents the Alias element are stored in a dictionary for further
  78. dereferencing during pointer linkage (see linkOpenPointer())."""
  79. aliases = {}
  80. for al in xmlelement.childNodes:
  81. if al.nodeType == al.ELEMENT_NODE:
  82. if al.hasAttribute("Alias"):
  83. aliasst = al.getAttribute("Alias")
  84. aliasnd = unicode(al.firstChild.data)
  85. aliases[aliasst] = aliasnd
  86. return aliases
  87. class NodeSet(object):
  88. """ This class handles parsing XML description of namespaces, instantiating
  89. nodes, linking references, graphing the namespace and compiling a binary
  90. representation.
  91. Note that nodes assigned to this class are not restricted to having a
  92. single namespace ID. This class represents the entire physical address
  93. space of the binary representation and all nodes that are to be included
  94. in that segment of memory.
  95. """
  96. def __init__(self):
  97. self.nodes = {}
  98. self.aliases = {}
  99. self.namespaces = ["http://opcfoundation.org/UA/"]
  100. def sanitize(self):
  101. for n in self.nodes.values():
  102. if n.sanitize() == False:
  103. raise Exception("Failed to sanitize node " + str(n))
  104. # Sanitize reference consistency
  105. for n in self.nodes.values():
  106. for ref in n.references:
  107. if not ref.source == n.id:
  108. raise Exception("Reference " + str(ref) + " has an invalid source")
  109. if not ref.referenceType in self.nodes:
  110. raise Exception("Reference " + str(ref) + " has an unknown reference type")
  111. if not ref.target in self.nodes:
  112. print(self.namespaces)
  113. raise Exception("Reference " + str(ref) + " has an unknown target")
  114. def addNamespace(self, nsURL):
  115. if not nsURL in self.namespaces:
  116. self.namespaces.append(nsURL)
  117. def createNamespaceMapping(self, orig_namespaces):
  118. """Creates a dict that maps from the nsindex in the original nodeset to the
  119. nsindex in the combined nodeset"""
  120. m = {}
  121. for index, name in enumerate(orig_namespaces):
  122. m[index] = self.namespaces.index(name)
  123. return m
  124. def getNodeByBrowseName(self, idstring):
  125. return next((n for n in self.nodes.values() if idstring == n.browseName.name), None)
  126. def getRoot(self):
  127. return self.getNodeByBrowseName("Root")
  128. def createNode(self, xmlelement, modelUri, hidden=False):
  129. ndtype = xmlelement.localName.lower()
  130. if ndtype[:2] == "ua":
  131. ndtype = ndtype[2:]
  132. node = None
  133. if ndtype == 'variable':
  134. node = VariableNode(xmlelement)
  135. if ndtype == 'object':
  136. node = ObjectNode(xmlelement)
  137. if ndtype == 'method':
  138. node = MethodNode(xmlelement)
  139. if ndtype == 'objecttype':
  140. node = ObjectTypeNode(xmlelement)
  141. if ndtype == 'variabletype':
  142. node = VariableTypeNode(xmlelement)
  143. if ndtype == 'methodtype':
  144. node = MethodNode(xmlelement)
  145. if ndtype == 'datatype':
  146. node = DataTypeNode(xmlelement)
  147. if ndtype == 'referencetype':
  148. node = ReferenceTypeNode(xmlelement)
  149. if node is None:
  150. return None
  151. node.modelUri = modelUri
  152. node.hidden = hidden
  153. return node
  154. def hide_node(self, nodeId, hidden=True):
  155. if not nodeId in self.nodes:
  156. return False
  157. node = self.nodes[nodeId]
  158. node.hidden = hidden
  159. return True
  160. def merge_dicts(self, *dict_args):
  161. """
  162. Given any number of dicts, shallow copy and merge into a new dict,
  163. precedence goes to key value pairs in latter dicts.
  164. """
  165. result = {}
  166. for dictionary in dict_args:
  167. result.update(dictionary)
  168. return result
  169. def getNodeByIDString(self, idStr):
  170. # Split id to namespace part and id part
  171. m = re.match("ns=([^;]+);(.*)", idStr)
  172. if m:
  173. ns = m.group(1)
  174. # Convert namespace uri to index
  175. if not ns.isdigit():
  176. if ns not in self.namespaces:
  177. return None
  178. ns = self.namespaces.index(ns)
  179. idStr = "ns={};{}".format(ns, m.group(2))
  180. nodeId = NodeId(idStr)
  181. if not nodeId in self.nodes:
  182. return None
  183. return self.nodes[nodeId]
  184. def remove_node(self, node):
  185. def filterRef(r, rt):
  186. return (r.referenceType != rt.referenceType) or (not (
  187. rt.target == node.id or rt.source == node.id
  188. ))
  189. for r in node.references:
  190. if r.target == node.id:
  191. if r.source not in self.nodes:
  192. continue
  193. self.nodes[r.source].references = set(filter(
  194. lambda rt: filterRef(r, rt),
  195. self.nodes[r.source].references
  196. ))
  197. elif r.source == node.id:
  198. if r.target not in self.nodes:
  199. continue
  200. self.nodes[r.target].references = set(filter(
  201. lambda rt: filterRef(r, rt),
  202. self.nodes[r.target].references
  203. ))
  204. del self.nodes[node.id]
  205. def addNodeSet(self, xmlfile, hidden=False, typesArray="UA_TYPES"):
  206. # Extract NodeSet DOM
  207. fileContent = xmlfile.read()
  208. # Remove BOM since the dom parser cannot handle it on python 3 windows
  209. if fileContent.startswith( codecs.BOM_UTF8 ):
  210. fileContent = fileContent.lstrip( codecs.BOM_UTF8 )
  211. if (sys.version_info >= (3, 0)):
  212. fileContent = fileContent.decode("utf-8")
  213. # Remove the uax namespace from tags. UaModeler adds this namespace to some elements
  214. fileContent = re.sub(r"<([/]?)uax:(.+?)([/]?)>", "<\g<1>\g<2>\g<3>>", fileContent)
  215. nodesets = dom.parseString(fileContent).getElementsByTagName("UANodeSet")
  216. if len(nodesets) == 0 or len(nodesets) > 1:
  217. raise Exception(self, self.originXML + " contains no or more then 1 nodeset")
  218. nodeset = nodesets[0]
  219. # Extract the modelUri
  220. try:
  221. modelTag = nodeset.getElementsByTagName("Models")[0].getElementsByTagName("Model")[0]
  222. modelUri = modelTag.attributes["ModelUri"].nodeValue
  223. except Exception:
  224. # Ignore exception and try to use namespace array
  225. modelUri = None
  226. # Create the namespace mapping
  227. orig_namespaces = extractNamespaces(xmlfile) # List of namespaces used in the xml file
  228. if modelUri is None and len(orig_namespaces) > 1:
  229. modelUri = orig_namespaces[1]
  230. if modelUri is None:
  231. raise Exception(self, self.originXML + " does not define the nodeset URI in Models/Model/ModelUri or NamespaceUris array.")
  232. for ns in orig_namespaces:
  233. self.addNamespace(ns)
  234. namespaceMapping = self.createNamespaceMapping(orig_namespaces) # mapping for this file
  235. # Extract the aliases
  236. for nd in nodeset.childNodes:
  237. if nd.nodeType != nd.ELEMENT_NODE:
  238. continue
  239. ndtype = nd.localName.lower()
  240. if 'aliases' in ndtype:
  241. self.aliases = self.merge_dicts(self.aliases, buildAliasList(nd))
  242. # Instantiate nodes
  243. newnodes = {}
  244. for nd in nodeset.childNodes:
  245. if nd.nodeType != nd.ELEMENT_NODE:
  246. continue
  247. node = self.createNode(nd, modelUri, hidden)
  248. if not node:
  249. continue
  250. node.replaceAliases(self.aliases)
  251. node.replaceNamespaces(namespaceMapping)
  252. node.typesArray = typesArray
  253. # Add the node the the global dict
  254. if node.id in self.nodes:
  255. raise Exception("XMLElement with duplicate ID " + str(node.id))
  256. self.nodes[node.id] = node
  257. newnodes[node.id] = node
  258. # Parse Datatypes in order to find out what the XML keyed values actually
  259. # represent.
  260. # Ex. <rpm>123</rpm> is not encodable
  261. # only after parsing the datatypes, it is known that
  262. # rpm is encoded as a double
  263. for n in newnodes.values():
  264. if isinstance(n, DataTypeNode):
  265. n.buildEncoding(self, namespaceMapping=namespaceMapping)
  266. def getBinaryEncodingIdForNode(self, nodeId):
  267. """
  268. The node should have a 'HasEncoding' forward reference which points to the encoding ids.
  269. These can be XML Encoding or Binary Encoding. Therefore we also need to check if the SymbolicName
  270. of the target node is "DefaultBinary"
  271. """
  272. node = self.nodes[nodeId]
  273. for ref in node.references:
  274. if ref.referenceType.ns == 0 and ref.referenceType.i == 38:
  275. refNode = self.nodes[ref.target]
  276. if refNode.symbolicName.value == "DefaultBinary":
  277. return ref.target
  278. raise Exception("No DefaultBinary encoding defined for node " + str(nodeId))
  279. def allocateVariables(self):
  280. for n in self.nodes.values():
  281. if isinstance(n, VariableNode):
  282. n.allocateValue(self)
  283. def getBaseDataType(self, node):
  284. if node is None:
  285. return None
  286. if node.browseName.name not in opaque_type_mapping:
  287. return node
  288. for ref in node.references:
  289. if ref.isForward:
  290. continue
  291. if ref.referenceType.i == 45:
  292. return self.getBaseDataType(self.nodes[ref.target])
  293. return node
  294. def getNodeTypeDefinition(self, node):
  295. for ref in node.references:
  296. # 40 = HasTypeDefinition
  297. if ref.referenceType.i == 40 and ref.isForward:
  298. return self.nodes[ref.target]
  299. return None
  300. def getDataTypeNode(self, dataType):
  301. if isinstance(dataType, string_types):
  302. if not valueIsInternalType(dataType):
  303. logger.error("Not a valid dataType string: " + dataType)
  304. return None
  305. return self.nodes[NodeId(self.aliases[dataType])]
  306. if isinstance(dataType, NodeId):
  307. if dataType.i == 0:
  308. return None
  309. dataTypeNode = self.nodes[dataType]
  310. if not isinstance(dataTypeNode, DataTypeNode):
  311. logger.error("Node id " + str(dataType) + " is not reference a valid dataType.")
  312. return None
  313. return dataTypeNode
  314. return None
  315. def getRelevantOrderingReferences(self):
  316. relevant_types = set()
  317. relevant_types.update(getSubTypesOf(self, self.getNodeByBrowseName("HierarchicalReferences"), []))
  318. relevant_types.update(getSubTypesOf(self, self.getNodeByBrowseName("HasEncoding"), []))
  319. relevant_types.update(getSubTypesOf(self, self.getNodeByBrowseName("HasTypeDefinition"), []))
  320. return list(map(lambda x: x.id, relevant_types))
  321. def addInverseReferences(self):
  322. # Ensure that every reference has an inverse reference in the target
  323. for u in self.nodes.values():
  324. for ref in u.references:
  325. back = Reference(ref.target, ref.referenceType, ref.source, not ref.isForward)
  326. self.nodes[ref.target].references.add(back) # ref set does not make a duplicate entry
  327. def setNodeParent(self):
  328. parentreftypes = getSubTypesOf(self, self.getNodeByBrowseName("HierarchicalReferences"))
  329. parentreftypes = list(map(lambda x: x.id, parentreftypes))
  330. for node in self.nodes.values():
  331. if node.id.ns == 0 and node.id.i in [78, 80, 84]:
  332. # ModellingRule, Root node do not have a parent
  333. continue
  334. parentref = node.getParentReference(parentreftypes)
  335. if parentref is not None:
  336. node.parent = self.nodes[parentref.target]
  337. if not node.parent:
  338. raise RuntimeError("Node {}: Did not find parent node: ".format(str(node.id)))
  339. node.parentReference = self.nodes[parentref.referenceType]
  340. # Some nodes in the full nodeset do not have a parent. So accept this and do not show an error.
  341. #else:
  342. # raise RuntimeError("Node {}: HierarchicalReference (or subtype of it) to parent node is missing.".format(str(node.id)))