metadata_parser_xml.py revision 1dd4ecb0ea0589610b3616459b707c2898889153
1#!/usr/bin/python
2
3#
4# Copyright (C) 2012 The Android Open Source Project
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#      http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19"""
20A parser for metadata_properties.xml can also render the resulting model
21over a Mako template.
22
23Usage:
24  metadata_parser_xml.py <filename.xml> <template.mako> [<output_file>]
25  - outputs the resulting template to output_file (stdout if none specified)
26
27Module:
28  The parser is also available as a module import (MetadataParserXml) to use
29  in other modules.
30
31Dependencies:
32  BeautifulSoup - an HTML/XML parser available to download from
33          http://www.crummy.com/software/BeautifulSoup/
34  Mako - a template engine for Python, available to download from
35     http://www.makotemplates.org/
36"""
37
38import sys
39import os
40import StringIO
41
42from bs4 import BeautifulSoup
43from bs4 import NavigableString
44
45from mako.template import Template
46from mako.lookup import TemplateLookup
47from mako.runtime import Context
48
49from metadata_model import *
50import metadata_model
51from metadata_validate import *
52import metadata_helpers
53
54class MetadataParserXml:
55  """
56  A class to parse any XML file that passes validation with metadata-validate.
57  It builds a metadata_model.Metadata graph and then renders it over a
58  Mako template.
59
60  Attributes (Read-Only):
61    soup: an instance of BeautifulSoup corresponding to the XML contents
62    metadata: a constructed instance of metadata_model.Metadata
63  """
64  def __init__(self, file_name):
65    """
66    Construct a new MetadataParserXml, immediately try to parse it into a
67    metadata model.
68
69    Args:
70      file_name: path to an XML file that passes metadata-validate
71
72    Raises:
73      ValueError: if the XML file failed to pass metadata_validate.py
74    """
75    self._soup = validate_xml(file_name)
76
77    if self._soup is None:
78      raise ValueError("%s has an invalid XML file" %(file_name))
79
80    self._metadata = Metadata()
81    self._parse()
82    self._metadata.construct_graph()
83
84  @property
85  def soup(self):
86    return self._soup
87
88  @property
89  def metadata(self):
90    return self._metadata
91
92  @staticmethod
93  def _find_direct_strings(element):
94    if element.string is not None:
95      return [element.string]
96
97    return [i for i in element.contents if isinstance(i, NavigableString)]
98
99  @staticmethod
100  def _strings_no_nl(element):
101    return "".join([i.strip() for i in MetadataParserXml._find_direct_strings(element)])
102
103  def _parse(self):
104
105    tags = self.soup.tags
106    if tags is not None:
107      for tag in tags.find_all('tag'):
108        self.metadata.insert_tag(tag['id'], tag.string)
109
110    types = self.soup.types
111    if types is not None:
112      for tp in types.find_all('typedef'):
113        languages = {}
114        for lang in tp.find_all('language'):
115          languages[lang['name']] = lang.string
116
117        self.metadata.insert_type(tp['name'], 'typedef', languages=languages)
118
119    # add all entries, preserving the ordering of the XML file
120    # this is important for future ABI compatibility when generating code
121    entry_filter = lambda x: x.name == 'entry' or x.name == 'clone'
122    for entry in self.soup.find_all(entry_filter):
123      if entry.name == 'entry':
124        d = {
125              'name': fully_qualified_name(entry),
126              'type': entry['type'],
127              'kind': find_kind(entry),
128              'type_notes': entry.attrs.get('type_notes')
129            }
130
131        d2 = self._parse_entry(entry)
132        insert = self.metadata.insert_entry
133      else:
134        d = {
135           'name': entry['entry'],
136           'kind': find_kind(entry),
137           'target_kind': entry['kind'],
138          # no type since its the same
139          # no type_notes since its the same
140        }
141        d2 = {}
142
143        insert = self.metadata.insert_clone
144
145      d3 = self._parse_entry_optional(entry)
146
147      entry_dict = dict(d.items() + d2.items() + d3.items())
148      insert(entry_dict)
149
150    self.metadata.construct_graph()
151
152  def _parse_entry(self, entry):
153    d = {}
154
155    #
156    # Visibility
157    #
158    d['visibility'] = entry.get('visibility')
159
160    #
161    # Optional for non-full hardware level devices
162    #
163    d['optional'] = entry.get('optional') == 'true'
164
165    #
166    # Typedef
167    #
168    d['type_name'] = entry.get('typedef')
169
170    #
171    # Enum
172    #
173    if entry.get('enum', 'false') == 'true':
174
175      enum_values = []
176      enum_optionals = []
177      enum_notes = {}
178      enum_ids = {}
179      for value in entry.enum.find_all('value'):
180
181        value_body = self._strings_no_nl(value)
182        enum_values.append(value_body)
183
184        if value.attrs.get('optional', 'false') == 'true':
185          enum_optionals.append(value_body)
186
187        notes = value.find('notes')
188        if notes is not None:
189          enum_notes[value_body] = notes.string
190
191        if value.attrs.get('id') is not None:
192          enum_ids[value_body] = value['id']
193
194      d['enum_values'] = enum_values
195      d['enum_optionals'] = enum_optionals
196      d['enum_notes'] = enum_notes
197      d['enum_ids'] = enum_ids
198      d['enum'] = True
199
200    #
201    # Container (Array/Tuple)
202    #
203    if entry.attrs.get('container') is not None:
204      container_name = entry['container']
205
206      array = entry.find('array')
207      if array is not None:
208        array_sizes = []
209        for size in array.find_all('size'):
210          array_sizes.append(size.string)
211        d['container_sizes'] = array_sizes
212
213      tupl = entry.find('tuple')
214      if tupl is not None:
215        tupl_values = []
216        for val in tupl.find_all('value'):
217          tupl_values.append(val.name)
218        d['tuple_values'] = tupl_values
219        d['container_sizes'] = len(tupl_values)
220
221      d['container'] = container_name
222
223    return d
224
225  def _parse_entry_optional(self, entry):
226    d = {}
227
228    optional_elements = ['description', 'range', 'units', 'notes']
229    for i in optional_elements:
230      prop = find_child_tag(entry, i)
231
232      if prop is not None:
233        d[i] = prop.string
234
235    tag_ids = []
236    for tag in entry.find_all('tag'):
237      tag_ids.append(tag['id'])
238
239    d['tag_ids'] = tag_ids
240
241    return d
242
243  def render(self, template, output_name=None):
244    """
245    Render the metadata model using a Mako template as the view.
246
247    The template gets the metadata as an argument, as well as all
248    public attributes from the metadata_helpers module.
249
250    The output file is encoded with UTF-8.
251
252    Args:
253      template: path to a Mako template file
254      output_name: path to the output file, or None to use stdout
255    """
256    buf = StringIO.StringIO()
257    metadata_helpers._context_buf = buf
258
259    helpers = [(i, getattr(metadata_helpers, i))
260                for i in dir(metadata_helpers) if not i.startswith('_')]
261    helpers = dict(helpers)
262
263    lookup = TemplateLookup(directories=[os.getcwd()])
264    tpl = Template(filename=template, lookup=lookup)
265
266    ctx = Context(buf, metadata=self.metadata, **helpers)
267    tpl.render_context(ctx)
268
269    tpl_data = buf.getvalue()
270    metadata_helpers._context_buf = None
271    buf.close()
272
273    if output_name is None:
274      print tpl_data
275    else:
276      file(output_name, "w").write(tpl_data.encode('utf-8'))
277
278#####################
279#####################
280
281if __name__ == "__main__":
282  if len(sys.argv) <= 2:
283    print >> sys.stderr,                                                       \
284           "Usage: %s <filename.xml> <template.mako> [<output_file>]"          \
285           % (sys.argv[0])
286    sys.exit(0)
287
288  file_name = sys.argv[1]
289  template_name = sys.argv[2]
290  output_name = sys.argv[3] if len(sys.argv) > 3 else None
291  parser = MetadataParserXml(file_name)
292  parser.render(template_name, output_name)
293
294  sys.exit(0)
295