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