def generateParser(self, existing, infiles , bsdFile): self.all_files = [] import_bsd = [] type_bsd = [] for xmlfile in existing: if xmlfile.name == "deps/ua-nodeset/DI/Opc.Ua.Di.NodeSet2.xml": continue nodeset_base = open(xmlfile.name, "rb") #fileContent = xmlfile.read() fileContent = nodeset_base.read() # Remove BOM since the dom parser cannot handle it on python 3 windows if fileContent.startswith( codecs.BOM_UTF8 ): fileContent = fileContent.lstrip( codecs.BOM_UTF8 ) if (sys.version_info >= (3, 0)): fileContent = fileContent.decode("utf-8") # Remove the uax namespace from tags. UaModeler adds this namespace to some elements fileContent = re.sub(r"<([/]?)uax:(.+?)([/]?)>", "<\g<1>\g<2>\g<3>>", fileContent) nodesets = dom.parseString(fileContent).getElementsByTagName("UANodeSet") if len(nodesets) == 0 or len(nodesets) > 1: raise Exception("contains no or more then 1 nodeset") nodeset = nodesets[0] variableNodes = nodeset.getElementsByTagName("UAVariable") for nd in variableNodes: if (nd.hasAttribute("SymbolicName") and (re.match(r".*_BinarySchema", nd.attributes["SymbolicName"].nodeValue) or nd.attributes["SymbolicName"].nodeValue == "TypeDictionary_BinarySchema")) or (nd.hasAttribute("ParentNodeId") and not nd.hasAttribute("SymbolicName") and re.fullmatch(r"i=93", nd.attributes["ParentNodeId"].nodeValue)): type_content = nd.getElementsByTagName("Value")[0].getElementsByTagName("ByteString")[0] f = tempfile.NamedTemporaryFile(delete=False, suffix='.bsd') f.write(base64.b64decode(type_content.firstChild.nodeValue)) f.flush() self.all_files.append(f.name) f.close() bsd = "UA_TYPES#" + f.name import_bsd.append(bsd) for xmlfile in infiles: nodeset_base = open(xmlfile.name, "rb") #fileContent = xmlfile.read() fileContent = nodeset_base.read() # Remove BOM since the dom parser cannot handle it on python 3 windows if fileContent.startswith( codecs.BOM_UTF8 ): fileContent = fileContent.lstrip( codecs.BOM_UTF8 ) if (sys.version_info >= (3, 0)): fileContent = fileContent.decode("utf-8") # Remove the uax namespace from tags. UaModeler adds this namespace to some elements fileContent = re.sub(r"<([/]?)uax:(.+?)([/]?)>", "<\g<1>\g<2>\g<3>>", fileContent) nodesets = dom.parseString(fileContent).getElementsByTagName("UANodeSet") if len(nodesets) == 0 or len(nodesets) > 1: raise Exception("contains no or more then 1 nodeset") nodeset = nodesets[0] variableNodes = nodeset.getElementsByTagName("UAVariable") for nd in variableNodes: if (nd.hasAttribute("SymbolicName") and (re.match(r".*_BinarySchema", nd.attributes["SymbolicName"].nodeValue) or nd.attributes["SymbolicName"].nodeValue == "TypeDictionary_BinarySchema")) or (nd.hasAttribute("ParentNodeId") and not nd.hasAttribute("SymbolicName") and re.fullmatch(r"i=93", nd.attributes["ParentNodeId"].nodeValue)): type_content = nd.getElementsByTagName("Value")[0].getElementsByTagName("ByteString")[0] f = tempfile.NamedTemporaryFile(suffix='.bsd') f.write(base64.b64decode(type_content.firstChild.nodeValue)) f.flush() f.seek(0) bf = io.BufferedReader(f) self.all_files.append(f.name) tmp = io.TextIOWrapper(bf, 'UTF-8', newline='\n') tmp.mode = 'r' type_bsd.append(tmp) opaque_map = [] selected_types = [] type_csv = [] no_builtin = True outname = "outname" for bsd in bsdFile: type_bsd.append(bsd) self.parser = CSVBSDTypeParser(opaque_map, selected_types, no_builtin, outname, import_bsd, type_bsd, type_csv, self.namespaces) self.parser.create_types() nodeset_base.close()
class NodeSet(object): """ This class handles parsing XML description of namespaces, instantiating nodes, linking references, graphing the namespace and compiling a binary representation. Note that nodes assigned to this class are not restricted to having a single namespace ID. This class represents the entire physical address space of the binary representation and all nodes that are to be included in that segment of memory. """ def __init__(self): self.nodes = {} self.aliases = {} self.namespaces = ["http://opcfoundation.org/UA/"] self.namespaceMapping = {} def sanitize(self): for n in self.nodes.values(): if n.sanitize() == False: raise Exception("Failed to sanitize node " + str(n)) # Sanitize reference consistency for n in self.nodes.values(): for ref in n.references: if not ref.source == n.id: raise Exception("Reference " + str(ref) + " has an invalid source") if not ref.referenceType in self.nodes: raise Exception("Reference " + str(ref) + " has an unknown reference type") if not ref.target in self.nodes: print(self.namespaces) raise Exception("Reference " + str(ref) + " has an unknown target") def addNamespace(self, nsURL): if not nsURL in self.namespaces: self.namespaces.append(nsURL) def createNamespaceMapping(self, orig_namespaces): """Creates a dict that maps from the nsindex in the original nodeset to the nsindex in the combined nodeset""" self.namespaceMapping = {} for index, name in enumerate(orig_namespaces): self.namespaceMapping[index] = self.namespaces.index(name) def getNodeByBrowseName(self, idstring): return next((n for n in self.nodes.values() if idstring == n.browseName.name), None) def getRoot(self): return self.getNodeByBrowseName("Root") def createNode(self, xmlelement, modelUri, hidden=False): ndtype = xmlelement.localName.lower() if ndtype[:2] == "ua": ndtype = ndtype[2:] node = None if ndtype == 'variable': node = VariableNode(xmlelement) if ndtype == 'object': node = ObjectNode(xmlelement) if ndtype == 'method': node = MethodNode(xmlelement) if ndtype == 'objecttype': node = ObjectTypeNode(xmlelement) if ndtype == 'variabletype': node = VariableTypeNode(xmlelement) if ndtype == 'methodtype': node = MethodNode(xmlelement) if ndtype == 'datatype': node = DataTypeNode(xmlelement) if ndtype == 'referencetype': node = ReferenceTypeNode(xmlelement) if node is None: return None node.modelUri = modelUri node.hidden = hidden return node def hide_node(self, nodeId, hidden=True): if not nodeId in self.nodes: return False node = self.nodes[nodeId] node.hidden = hidden return True def merge_dicts(self, *dict_args): """ Given any number of dicts, shallow copy and merge into a new dict, precedence goes to key value pairs in latter dicts. """ result = {} for dictionary in dict_args: result.update(dictionary) return result def getNodeByIDString(self, idStr): # Split id to namespace part and id part m = re.match("ns=([^;]+);(.*)", idStr) if m: ns = m.group(1) # Convert namespace uri to index if not ns.isdigit(): if ns not in self.namespaces: return None ns = self.namespaces.index(ns) idStr = "ns={};{}".format(ns, m.group(2)) nodeId = NodeId(idStr) if not nodeId in self.nodes: return None return self.nodes[nodeId] def remove_node(self, node): def filterRef(r, rt): return (r.referenceType != rt.referenceType) or (not ( rt.target == node.id or rt.source == node.id )) for r in node.references: if r.target == node.id: if r.source not in self.nodes: continue self.nodes[r.source].references = set(filter( lambda rt: filterRef(r, rt), self.nodes[r.source].references )) elif r.source == node.id: if r.target not in self.nodes: continue self.nodes[r.target].references = set(filter( lambda rt: filterRef(r, rt), self.nodes[r.target].references )) del self.nodes[node.id] def addNodeSet(self, xmlfile, hidden=False, typesArray="UA_TYPES"): # Extract NodeSet DOM fileContent = xmlfile.read() # Remove BOM since the dom parser cannot handle it on Python 3 Windows if fileContent.startswith( codecs.BOM_UTF8 ): fileContent = fileContent.lstrip( codecs.BOM_UTF8 ) if (sys.version_info >= (3, 0)): fileContent = fileContent.decode("utf-8") # Remove the uax namespace from tags. UaModeler adds this namespace to some elements fileContent = re.sub(r"<([/]?)uax:(.+?)([/]?)>", "<\g<1>\g<2>\g<3>>", fileContent) nodesets = dom.parseString(fileContent).getElementsByTagName("UANodeSet") if len(nodesets) == 0 or len(nodesets) > 1: raise Exception(self, self.originXML + " contains no or more then 1 nodeset") nodeset = nodesets[0] # Extract the modelUri try: modelTag = nodeset.getElementsByTagName("Models")[0].getElementsByTagName("Model")[0] modelUri = modelTag.attributes["ModelUri"].nodeValue except Exception: # Ignore exception and try to use namespace array modelUri = None # Create the namespace mapping orig_namespaces = extractNamespaces(xmlfile) # List of namespaces used in the xml file if modelUri is None and len(orig_namespaces) > 1: modelUri = orig_namespaces[1] if modelUri is None: raise Exception(self, self.originXML + " does not define the nodeset URI in Models/Model/ModelUri or NamespaceUris array.") for ns in orig_namespaces: self.addNamespace(ns) self.createNamespaceMapping(orig_namespaces) # mapping for this file # Extract the aliases for nd in nodeset.childNodes: if nd.nodeType != nd.ELEMENT_NODE: continue ndtype = nd.localName.lower() if 'aliases' in ndtype: self.aliases = self.merge_dicts(self.aliases, buildAliasList(nd)) # Instantiate nodes newnodes = {} for nd in nodeset.childNodes: if nd.nodeType != nd.ELEMENT_NODE: continue node = self.createNode(nd, modelUri, hidden) if not node: continue node.replaceAliases(self.aliases) node.replaceNamespaces(self.namespaceMapping) node.typesArray = typesArray # Add the node the the global dict if node.id in self.nodes: raise Exception("XMLElement with duplicate ID " + str(node.id)) self.nodes[node.id] = node newnodes[node.id] = node # Parse Datatypes in order to find out what the XML keyed values actually # represent. # Ex. <rpm>123</rpm> is not encodable # only after parsing the datatypes, it is known that # rpm is encoded as a double for n in newnodes.values(): if isinstance(n, DataTypeNode): n.buildEncoding(self, namespaceMapping=self.namespaceMapping) def getBinaryEncodingIdForNode(self, nodeId): """ The node should have a 'HasEncoding' forward reference which points to the encoding ids. These can be XML Encoding or Binary Encoding. Therefore we also need to check if the SymbolicName of the target node is "DefaultBinary" """ node = self.nodes[nodeId] for ref in node.references: if ref.referenceType.ns == 0 and ref.referenceType.i == 38: refNode = self.nodes[ref.target] if refNode.symbolicName.value == "DefaultBinary": return ref.target raise Exception("No DefaultBinary encoding defined for node " + str(nodeId)) def allocateVariables(self): for n in self.nodes.values(): if isinstance(n, VariableNode): n.allocateValue(self) def getBaseDataType(self, node): if node is None: return None if node.browseName.name not in opaque_type_mapping: return node for ref in node.references: if ref.isForward: continue if ref.referenceType.i == 45: return self.getBaseDataType(self.nodes[ref.target]) return node def getNodeTypeDefinition(self, node): for ref in node.references: # 40 = HasTypeDefinition if ref.referenceType.i == 40 and ref.isForward: return self.nodes[ref.target] return None def getDataTypeNode(self, dataType): if isinstance(dataType, string_types): if not valueIsInternalType(dataType): logger.error("Not a valid dataType string: " + dataType) return None return self.nodes[NodeId(self.aliases[dataType])] if isinstance(dataType, NodeId): if dataType.i == 0: return None dataTypeNode = self.nodes[dataType] if not isinstance(dataTypeNode, DataTypeNode): logger.error("Node id " + str(dataType) + " is not reference a valid dataType.") return None return dataTypeNode return None def getRelevantOrderingReferences(self): relevant_types = set() relevant_types.update(getSubTypesOf(self, self.getNodeByBrowseName("HierarchicalReferences"), [])) relevant_types.update(getSubTypesOf(self, self.getNodeByBrowseName("HasEncoding"), [])) relevant_types.update(getSubTypesOf(self, self.getNodeByBrowseName("HasTypeDefinition"), [])) return list(map(lambda x: x.id, relevant_types)) def addInverseReferences(self): # Ensure that every reference has an inverse reference in the target for u in self.nodes.values(): for ref in u.references.copy(): back = Reference(ref.target, ref.referenceType, ref.source, not ref.isForward) self.nodes[ref.target].references.add(back) # ref set does not make a duplicate entry def setNodeParent(self): parentreftypes = getSubTypesOf(self, self.getNodeByBrowseName("HierarchicalReferences")) parentreftypes = list(map(lambda x: x.id, parentreftypes)) for node in self.nodes.values(): if node.id.ns == 0 and node.id.i in [78, 80, 84]: # ModellingRule, Root node do not have a parent continue parentref = node.getParentReference(parentreftypes) if parentref is not None: node.parent = self.nodes[parentref.target] if not node.parent: raise RuntimeError("Node {}: Did not find parent node: ".format(str(node.id))) node.parentReference = self.nodes[parentref.referenceType] # Some nodes in the full nodeset do not have a parent. So accept this and do not show an error. #else: # raise RuntimeError("Node {}: HierarchicalReference (or subtype of it) to parent node is missing.".format(str(node.id))) def generateParser(self, existing, infiles , bsdFile): self.all_files = [] import_bsd = [] type_bsd = [] for xmlfile in existing: if xmlfile.name == "deps/ua-nodeset/DI/Opc.Ua.Di.NodeSet2.xml": continue nodeset_base = open(xmlfile.name, "rb") #fileContent = xmlfile.read() fileContent = nodeset_base.read() # Remove BOM since the dom parser cannot handle it on python 3 windows if fileContent.startswith( codecs.BOM_UTF8 ): fileContent = fileContent.lstrip( codecs.BOM_UTF8 ) if (sys.version_info >= (3, 0)): fileContent = fileContent.decode("utf-8") # Remove the uax namespace from tags. UaModeler adds this namespace to some elements fileContent = re.sub(r"<([/]?)uax:(.+?)([/]?)>", "<\g<1>\g<2>\g<3>>", fileContent) nodesets = dom.parseString(fileContent).getElementsByTagName("UANodeSet") if len(nodesets) == 0 or len(nodesets) > 1: raise Exception("contains no or more then 1 nodeset") nodeset = nodesets[0] variableNodes = nodeset.getElementsByTagName("UAVariable") for nd in variableNodes: if (nd.hasAttribute("SymbolicName") and (re.match(r".*_BinarySchema", nd.attributes["SymbolicName"].nodeValue) or nd.attributes["SymbolicName"].nodeValue == "TypeDictionary_BinarySchema")) or (nd.hasAttribute("ParentNodeId") and not nd.hasAttribute("SymbolicName") and re.fullmatch(r"i=93", nd.attributes["ParentNodeId"].nodeValue)): type_content = nd.getElementsByTagName("Value")[0].getElementsByTagName("ByteString")[0] f = tempfile.NamedTemporaryFile(delete=False, suffix='.bsd') f.write(base64.b64decode(type_content.firstChild.nodeValue)) f.flush() self.all_files.append(f.name) f.close() bsd = "UA_TYPES#" + f.name import_bsd.append(bsd) for xmlfile in infiles: nodeset_base = open(xmlfile.name, "rb") #fileContent = xmlfile.read() fileContent = nodeset_base.read() # Remove BOM since the dom parser cannot handle it on python 3 windows if fileContent.startswith( codecs.BOM_UTF8 ): fileContent = fileContent.lstrip( codecs.BOM_UTF8 ) if (sys.version_info >= (3, 0)): fileContent = fileContent.decode("utf-8") # Remove the uax namespace from tags. UaModeler adds this namespace to some elements fileContent = re.sub(r"<([/]?)uax:(.+?)([/]?)>", "<\g<1>\g<2>\g<3>>", fileContent) nodesets = dom.parseString(fileContent).getElementsByTagName("UANodeSet") if len(nodesets) == 0 or len(nodesets) > 1: raise Exception("contains no or more then 1 nodeset") nodeset = nodesets[0] variableNodes = nodeset.getElementsByTagName("UAVariable") for nd in variableNodes: if (nd.hasAttribute("SymbolicName") and (re.match(r".*_BinarySchema", nd.attributes["SymbolicName"].nodeValue) or nd.attributes["SymbolicName"].nodeValue == "TypeDictionary_BinarySchema")) or (nd.hasAttribute("ParentNodeId") and not nd.hasAttribute("SymbolicName") and re.fullmatch(r"i=93", nd.attributes["ParentNodeId"].nodeValue)): type_content = nd.getElementsByTagName("Value")[0].getElementsByTagName("ByteString")[0] f = tempfile.NamedTemporaryFile(suffix='.bsd') f.write(base64.b64decode(type_content.firstChild.nodeValue)) f.flush() f.seek(0) bf = io.BufferedReader(f) self.all_files.append(f.name) tmp = io.TextIOWrapper(bf, 'UTF-8', newline='\n') tmp.mode = 'r' type_bsd.append(tmp) opaque_map = [] selected_types = [] type_csv = [] no_builtin = True outname = "outname" for bsd in bsdFile: type_bsd.append(bsd) self.parser = CSVBSDTypeParser(opaque_map, selected_types, no_builtin, outname, import_bsd, type_bsd, type_csv, self.namespaces) self.parser.create_types() nodeset_base.close()