nodeset.py 13 KB

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