nodeset.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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. from struct import pack as structpack
  21. from time import struct_time, strftime, strptime, mktime
  22. import logging;
  23. logger = logging.getLogger(__name__)
  24. from nodes import *
  25. from opaque_type_mapping import opaque_type_mapping
  26. ####################
  27. # Helper Functions #
  28. ####################
  29. hassubtype = NodeId("ns=0;i=45")
  30. def getSubTypesOf(nodeset, node, skipNodes=[]):
  31. if node in skipNodes:
  32. return []
  33. re = [node]
  34. for ref in node.references:
  35. if ref.referenceType == hassubtype and ref.isForward:
  36. re = re + getSubTypesOf(nodeset, nodeset.nodes[ref.target], skipNodes=skipNodes)
  37. return re
  38. def extractNamespaces(xmlfile):
  39. # Extract a list of namespaces used. The first namespace is always
  40. # "http://opcfoundation.org/UA/". minidom gobbles up
  41. # <NamespaceUris></NamespaceUris> elements, without a decent way to reliably
  42. # access this dom2 <uri></uri> elements (only attribute xmlns= are accessible
  43. # using minidom). We need them for dereferencing though... This function
  44. # attempts to do just that.
  45. namespaces = ["http://opcfoundation.org/UA/"]
  46. infile = open(xmlfile.name)
  47. foundURIs = False
  48. nsline = ""
  49. line = infile.readline()
  50. for line in infile:
  51. if "<namespaceuris>" in line.lower():
  52. foundURIs = True
  53. elif "</namespaceuris>" in line.lower():
  54. foundURIs = False
  55. nsline = nsline + line
  56. break
  57. if foundURIs:
  58. nsline = nsline + line
  59. if len(nsline) > 0:
  60. ns = dom.parseString(nsline).getElementsByTagName("NamespaceUris")
  61. for uri in ns[0].childNodes:
  62. if uri.nodeType != uri.ELEMENT_NODE:
  63. continue
  64. if uri.firstChild.data in namespaces:
  65. continue
  66. namespaces.append(uri.firstChild.data)
  67. infile.close()
  68. return namespaces
  69. def buildAliasList(xmlelement):
  70. """Parses the <Alias> XML Element present in must XML NodeSet definitions.
  71. Contents the Alias element are stored in a dictionary for further
  72. dereferencing during pointer linkage (see linkOpenPointer())."""
  73. aliases = {}
  74. for al in xmlelement.childNodes:
  75. if al.nodeType == al.ELEMENT_NODE:
  76. if al.hasAttribute("Alias"):
  77. aliasst = al.getAttribute("Alias")
  78. aliasnd = unicode(al.firstChild.data)
  79. aliases[aliasst] = aliasnd
  80. return aliases
  81. class NodeSet(object):
  82. """ This class handles parsing XML description of namespaces, instantiating
  83. nodes, linking references, graphing the namespace and compiling a binary
  84. representation.
  85. Note that nodes assigned to this class are not restricted to having a
  86. single namespace ID. This class represents the entire physical address
  87. space of the binary representation and all nodes that are to be included
  88. in that segment of memory.
  89. """
  90. def __init__(self):
  91. self.nodes = {}
  92. self.aliases = {}
  93. self.namespaces = ["http://opcfoundation.org/UA/"]
  94. def sanitize(self):
  95. for n in self.nodes.values():
  96. if n.sanitize() == False:
  97. raise Exception("Failed to sanitize node " + str(n))
  98. # Sanitize reference consistency
  99. for n in self.nodes.values():
  100. for ref in n.references:
  101. if not ref.source == n.id:
  102. raise Exception("Reference " + str(ref) + " has an invalid source")
  103. if not ref.referenceType in self.nodes:
  104. raise Exception("Reference " + str(ref) + " has an unknown reference type")
  105. if not ref.target in self.nodes:
  106. raise Exception("Reference " + str(ref) + " has an unknown target")
  107. def addNamespace(self, nsURL):
  108. if not nsURL in self.namespaces:
  109. self.namespaces.append(nsURL)
  110. def createNamespaceMapping(self, orig_namespaces):
  111. """Creates a dict that maps from the nsindex in the original nodeset to the
  112. nsindex in the combined nodeset"""
  113. m = {}
  114. for index, name in enumerate(orig_namespaces):
  115. m[index] = self.namespaces.index(name)
  116. return m
  117. def getNodeByBrowseName(self, idstring):
  118. return next((n for n in self.nodes.values() if idstring == n.browseName.name), None)
  119. def getNodeById(self, namespace, id):
  120. nodeId = NodeId()
  121. nodeId.ns = namespace
  122. nodeId.i = id
  123. return self.nodes[nodeId]
  124. def getRoot(self):
  125. return self.getNodeByBrowseName("Root")
  126. def createNode(self, xmlelement, nsMapping, hidden=False):
  127. ndtype = xmlelement.localName.lower()
  128. if ndtype[:2] == "ua":
  129. ndtype = ndtype[2:]
  130. node = None
  131. if ndtype == 'variable':
  132. node = VariableNode(xmlelement)
  133. if ndtype == 'object':
  134. node = ObjectNode(xmlelement)
  135. if ndtype == 'method':
  136. node = MethodNode(xmlelement)
  137. if ndtype == 'objecttype':
  138. node = ObjectTypeNode(xmlelement)
  139. if ndtype == 'variabletype':
  140. node = VariableTypeNode(xmlelement)
  141. if ndtype == 'methodtype':
  142. node = MethodNode(xmlelement)
  143. if ndtype == 'datatype':
  144. node = DataTypeNode(xmlelement)
  145. if ndtype == 'referencetype':
  146. node = ReferenceTypeNode(xmlelement)
  147. if node and hidden:
  148. node.hidden = True
  149. # References from an existing nodeset are all suppressed
  150. for ref in node.references:
  151. ref.hidden = True
  152. for ref in node.inverseReferences:
  153. ref.hidden = True
  154. return node
  155. def hide_node(self, nodeId, hidden=True):
  156. if not nodeId in self.nodes:
  157. return False
  158. node = self.nodes[nodeId]
  159. node.hidden = hidden
  160. # References from an existing nodeset are all suppressed
  161. for ref in node.references:
  162. ref.hidden = hidden
  163. for ref in node.inverseReferences:
  164. ref.hidden = hidden
  165. return True
  166. def merge_dicts(self, *dict_args):
  167. """
  168. Given any number of dicts, shallow copy and merge into a new dict,
  169. precedence goes to key value pairs in latter dicts.
  170. """
  171. result = {}
  172. for dictionary in dict_args:
  173. result.update(dictionary)
  174. return result
  175. def addNodeSet(self, xmlfile, hidden=False, typesArray="UA_TYPES"):
  176. # Extract NodeSet DOM
  177. nodesets = dom.parse(xmlfile).getElementsByTagName("UANodeSet")
  178. if len(nodesets) == 0 or len(nodesets) > 1:
  179. raise Exception(self, self.originXML + " contains no or more then 1 nodeset")
  180. nodeset = nodesets[0]
  181. # Create the namespace mapping
  182. orig_namespaces = extractNamespaces(xmlfile) # List of namespaces used in the xml file
  183. for ns in orig_namespaces:
  184. self.addNamespace(ns)
  185. nsMapping = self.createNamespaceMapping(orig_namespaces)
  186. # Extract the aliases
  187. for nd in nodeset.childNodes:
  188. if nd.nodeType != nd.ELEMENT_NODE:
  189. continue
  190. ndtype = nd.localName.lower()
  191. if 'aliases' in ndtype:
  192. self.aliases = self.merge_dicts(self.aliases, buildAliasList(nd))
  193. # Instantiate nodes
  194. newnodes = []
  195. for nd in nodeset.childNodes:
  196. if nd.nodeType != nd.ELEMENT_NODE:
  197. continue
  198. node = self.createNode(nd, nsMapping, hidden)
  199. if not node:
  200. continue
  201. node.replaceAliases(self.aliases)
  202. node.replaceNamespaces(nsMapping)
  203. node.typesArray = typesArray
  204. # Add the node the the global dict
  205. if node.id in self.nodes:
  206. raise Exception("XMLElement with duplicate ID " + str(node.id))
  207. self.nodes[node.id] = node
  208. newnodes.append(node)
  209. # add inverse references
  210. for node in newnodes:
  211. for ref in node.references:
  212. newsource = self.nodes[ref.target]
  213. hide = ref.hidden or (node.hidden and newsource.hidden)
  214. newref = Reference(newsource.id, ref.referenceType, ref.source, False, hide, inferred=True)
  215. newsource.inverseReferences.add(newref)
  216. for ref in node.inverseReferences:
  217. newsource = self.nodes[ref.target]
  218. hide = ref.hidden or (node.hidden and newsource.hidden)
  219. newref = Reference(newsource.id, ref.referenceType, ref.source, True, hide, inferred=True)
  220. newsource.references.add(newref)
  221. def getBinaryEncodingIdForNode(self, nodeId):
  222. """
  223. The node should have a 'HasEncoding' forward reference which points to the encoding ids.
  224. These can be XML Encoding or Binary Encoding. Therefore we also need to check if the SymbolicName
  225. of the target node is "DefaultBinary"
  226. """
  227. node = self.nodes[nodeId]
  228. refId = NodeId()
  229. for ref in node.references:
  230. if ref.referenceType.ns == 0 and ref.referenceType.i == 38:
  231. refNode = self.nodes[ref.target]
  232. if refNode.symbolicName.value == "DefaultBinary":
  233. return ref.target
  234. raise Exception("No DefaultBinary encoding defined for node " + str(nodeId))
  235. def buildEncodingRules(self):
  236. """ Calls buildEncoding() for all DataType nodes (opcua_node_dataType_t).
  237. No return value
  238. """
  239. stat = {True: 0, False: 0}
  240. for n in self.nodes.values():
  241. if isinstance(n, DataTypeNode):
  242. n.buildEncoding(self)
  243. stat[n.isEncodable()] = stat[n.isEncodable()] + 1
  244. logger.debug("Type definitions built/passed: " + str(stat))
  245. def allocateVariables(self):
  246. for n in self.nodes.values():
  247. if isinstance(n, VariableNode):
  248. n.allocateValue(self)
  249. def getBaseDataType(self, node):
  250. if node is None:
  251. return None
  252. if node.browseName.name not in opaque_type_mapping:
  253. return node
  254. for ref in node.inverseReferences:
  255. if ref.referenceType.i == 45:
  256. return self.getBaseDataType(self.nodes[ref.target])
  257. return node
  258. def getDataTypeNode(self, dataType):
  259. if isinstance(dataType, basestring):
  260. if not valueIsInternalType(dataType):
  261. logger.error("Not a valid dataType string: " + dataType)
  262. return None
  263. return self.nodes[NodeId(self.aliases[dataType])]
  264. if isinstance(dataType, NodeId):
  265. if dataType.i == 0:
  266. return None
  267. dataTypeNode = self.nodes[dataType]
  268. if not isinstance(dataTypeNode, DataTypeNode):
  269. logger.error("Node id " + str(dataType) + " is not reference a valid dataType.")
  270. return None
  271. if not dataTypeNode.isEncodable():
  272. logger.warn("DataType " + str(dataTypeNode.browseName) + " is not encodable.")
  273. return dataTypeNode
  274. return None
  275. def getRelevantOrderingReferences(self):
  276. relevant_types = getSubTypesOf(self,
  277. self.getNodeByBrowseName("HierarchicalReferences"),
  278. [])
  279. relevant_types += getSubTypesOf(self,
  280. self.getNodeByBrowseName("HasEncoding"),
  281. [])
  282. relevant_types = map(lambda x: x.id, relevant_types)
  283. return relevant_types