1# Software License Agreement (BSD License) 2# 3# Copyright (c) 2012, Willow Garage, Inc. 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions 8# are met: 9# 10# * Redistributions of source code must retain the above copyright 11# notice, this list of conditions and the following disclaimer. 12# * Redistributions in binary form must reproduce the above 13# copyright notice, this list of conditions and the following 14# disclaimer in the documentation and/or other materials provided 15# with the distribution. 16# * Neither the name of Willow Garage, Inc. nor the names of its 17# contributors may be used to endorse or promote products derived 18# from this software without specific prior written permission. 19# 20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31# POSSIBILITY OF SUCH DAMAGE. 32 33""" 34Library for processing stack.xml created post-catkin 35""" 36 37import collections 38import os 39import xml.dom.minidom as dom 40 41# as defined on http://ros.org/doc/fuerte/api/catkin/html/stack_xml.html 42REQUIRED = ['name', 'version', 'description', 'author', 'maintainer', 'license', 'copyright'] 43ALLOWXHTML = ['description'] 44OPTIONAL = ['description_brief', 'version_abi', 'url', 'review_notes', 'review_status', 'build_depends', 'depends', 'build_type', 'message_generator', 'review'] 45 46LISTED_ATTRIBUTES = {'Author': ['name', 'email'], 'Maintainer': ['name', 'email'], 'Depend': ['name', 'version']} 47 48VALID = REQUIRED + OPTIONAL 49 50 51class InvalidStack(Exception): 52 pass 53 54 55def _get_nodes_by_name(n, name): 56 return [t for t in n.childNodes if t.nodeType == t.ELEMENT_NODE and t.tagName == name] 57 58 59def _check_optional(name, allowXHTML=False): 60 """ 61 Validator for optional elements. 62 63 :raise: :exc:`InvalidStack` If validation fails 64 """ 65 def check(n, filename): 66 n = _get_nodes_by_name(n, name) 67 if len(n) > 1: 68 raise InvalidStack("Invalid stack.xml file [%s]: must have at most one '%s' element" % (filename, name)) 69 if n: 70 if allowXHTML: 71 return ''.join([x.toxml() for x in n[0].childNodes]) 72 return _get_text(n[0].childNodes).strip() 73 return check 74 75 76def _check_required(name, allowXHTML=False): 77 """ 78 Validator for required elements. 79 80 :raise: :exc:`InvalidStack` If validation fails 81 """ 82 def check(n, filename): 83 n = _get_nodes_by_name(n, name) 84 if len(n) != 1: 85 raise InvalidStack("Invalid stack.xml file [%s]: must have exactly one '%s' element" % (filename, name)) 86 if allowXHTML: 87 return ''.join([x.toxml() for x in n[0].childNodes]) 88 return _get_text(n[0].childNodes).strip() 89 return check 90 91 92def _check_depends(n, key, filename): 93 """ 94 Validator for stack.xml depends. 95 :raise: :exc:`InvalidStack` If validation fails 96 """ 97 nodes = _get_nodes_by_name(n, key) 98 return set([_get_text(n.childNodes).strip() for n in nodes]) 99 100 101def _build_listed_attributes(n, key, object_type): 102 """ 103 Validator for stack.xml depends. 104 :raise: :exc:`InvalidStack` If validation fails 105 """ 106 members = set() 107 for node in _get_nodes_by_name(n, key): 108 # The first field is always supposed to be the value 109 attribute_dict = {} 110 for field in object_type._fields: 111 try: 112 attribute_dict[field] = node.getAttribute(field) 113 except: 114 pass 115 attribute_dict[object_type._fields[0]] = _get_text(node.childNodes).strip() 116 members.add(object_type(**attribute_dict)) 117 return members 118 119 120def _attrs(node): 121 attrs = {} 122 for k in node.attributes.keys(): 123 attrs[k] = node.attributes.get(k).value 124 return attrs 125 126 127def _check(name): 128 """ 129 Generic validator for text-based tags. 130 """ 131 if name in REQUIRED: 132 return _check_required(name, name in ALLOWXHTML) 133 elif name in OPTIONAL: 134 return _check_optional(name, name in ALLOWXHTML) 135 136 137class Stack(object): 138 """ 139 Object representation of a ROS ``stack.xml`` file 140 """ 141 __slots__ = [ 142 'name', 'version', 'description', 'authors', 'maintainers', 'license', 'copyright', 143 'description_brief', 'version_abi', 'url', 'review_notes', 'review_status', 144 'build_depends', 'depends', 'build_type', 'build_type_file', 'message_generator', 145 'unknown_tags'] 146 147 def __init__(self, filename=None): 148 """ 149 :param filename: location of stack.xml. Necessary if 150 converting ``${prefix}`` in ``<export>`` values, ``str``. 151 """ 152 self.description = self.description_brief = self.name = \ 153 self.version = self.version_abi = \ 154 self.license = self.copyright = '' 155 self.url = '' 156 self.authors = [] 157 self.maintainers = [] 158 self.depends = [] 159 self.build_depends = [] 160 self.review_notes = self.review_status = '' 161 self.build_type = 'cmake' 162 self.build_type_file = '' 163 self.message_generator = '' 164 165 # store unrecognized tags during parsing 166 self.unknown_tags = [] 167 168 169def _get_text(nodes): 170 """ 171 DOM utility routine for getting contents of text nodes 172 """ 173 return "".join([n.data for n in nodes if n.nodeType == n.TEXT_NODE]) 174 175 176def parse_stack_file(stack_path): 177 """ 178 Parse stack file. 179 180 :param stack_path: The path of the stack.xml file 181 182 :returns: return :class:`Stack` instance, populated with parsed fields 183 :raises: :exc:`InvalidStack` 184 :raises: :exc:`IOError` 185 """ 186 if not os.path.isfile(stack_path): 187 raise IOError("Invalid/non-existent stack.xml file: %s" % (stack_path)) 188 189 with open(stack_path, 'r') as f: 190 return parse_stack(f.read(), stack_path) 191 192 193def parse_stack(string, filename): 194 """ 195 Parse stack.xml string contents. 196 197 :param string: stack.xml contents, ``str`` 198 :param filename: full file path for debugging, ``str`` 199 :returns: return parsed :class:`Stack` 200 """ 201 # Create some classes to hold some members 202 new_tuples = {} 203 for key, members in LISTED_ATTRIBUTES.items(): 204 new_tuples[key] = collections.namedtuple(key, members) 205 206 try: 207 d = dom.parseString(string) 208 except Exception as e: 209 raise InvalidStack("[%s] invalid XML: %s" % (filename, e)) 210 211 s = Stack() 212 p = _get_nodes_by_name(d, 'stack') 213 if len(p) != 1: 214 raise InvalidStack("stack.xml [%s] must have a single 'stack' element" % (filename)) 215 p = p[0] 216 for attr in [ 217 'name', 'version', 'description', 218 'license', 'copyright', 'url', 'build_type', 'message_generator' 219 ]: 220 val = _check(attr)(p, filename) 221 if val: 222 setattr(s, attr, val) 223 224 try: 225 tag = _get_nodes_by_name(p, 'description')[0] 226 s.description_brief = tag.getAttribute('brief') or '' 227 except: 228 # means that 'description' tag is missing 229 pass 230 231 s.authors = _build_listed_attributes(p, 'author', new_tuples['Author']) 232 s.maintainers = _build_listed_attributes(p, 'maintainer', new_tuples['Maintainer']) 233 s.depends = _build_listed_attributes(p, 'depends', new_tuples['Depend']) 234 s.build_depends = _build_listed_attributes(p, 'build_depends', new_tuples['Depend']) 235 236 try: 237 tag = _get_nodes_by_name(p, 'review')[0] 238 s.review_status = tag.getAttribute('status') or '' 239 except: 240 pass # stack.xml is missing optional 'review status' tag 241 242 try: 243 tag = _get_nodes_by_name(p, 'review')[0] 244 s.review_notes = tag.getAttribute('notes') or '' 245 except: 246 pass # stack.xml is missing optional 'review notes' tag 247 248 try: 249 tag = _get_nodes_by_name(p, 'build_type')[0] 250 s.build_type_file = tag.getAttribute('file') or '' 251 except: 252 pass # stack.xml is missing optional 'build_type file' tag 253 254 # store unrecognized tags 255 s.unknown_tags = [e.nodeName for e in p.childNodes if e.nodeType == e.ELEMENT_NODE and e.tagName not in VALID] 256 if s.unknown_tags: 257 raise InvalidStack("stack.xml [%s] must be cleaned up from %s" % (filename, str(s.unknown_tags))) 258 return s 259