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 block 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, xml, file_name):
65    """
66    Construct a new MetadataParserXml, immediately try to parse it into a
67    metadata model.
68
69    Args:
70      xml: The XML block to use for the metadata
71      file_name: Source of the XML block, only for debugging/errors
72
73    Raises:
74      ValueError: if the XML block failed to pass metadata_validate.py
75    """
76    self._soup = validate_xml(xml)
77
78    if self._soup is None:
79      raise ValueError("%s has an invalid XML file" % (file_name))
80
81    self._metadata = Metadata()
82    self._parse()
83    self._metadata.construct_graph()
84
85  @staticmethod
86  def create_from_file(file_name):
87    """
88    Construct a new MetadataParserXml by loading and parsing an XML file.
89
90    Args:
91      file_name: Name of the XML file to load and parse.
92
93    Raises:
94      ValueError: if the XML file failed to pass metadata_validate.py
95
96    Returns:
97      MetadataParserXml instance representing the XML file.
98    """
99    return MetadataParserXml(file(file_name).read(), file_name)
100
101  @property
102  def soup(self):
103    return self._soup
104
105  @property
106  def metadata(self):
107    return self._metadata
108
109  @staticmethod
110  def _find_direct_strings(element):
111    if element.string is not None:
112      return [element.string]
113
114    return [i for i in element.contents if isinstance(i, NavigableString)]
115
116  @staticmethod
117  def _strings_no_nl(element):
118    return "".join([i.strip() for i in MetadataParserXml._find_direct_strings(element)])
119
120  def _parse(self):
121
122    tags = self.soup.tags
123    if tags is not None:
124      for tag in tags.find_all('tag'):
125        self.metadata.insert_tag(tag['id'], tag.string)
126
127    types = self.soup.types
128    if types is not None:
129      for tp in types.find_all('typedef'):
130        languages = {}
131        for lang in tp.find_all('language'):
132          languages[lang['name']] = lang.string
133
134        self.metadata.insert_type(tp['name'], 'typedef', languages=languages)
135
136    # add all entries, preserving the ordering of the XML file
137    # this is important for future ABI compatibility when generating code
138    entry_filter = lambda x: x.name == 'entry' or x.name == 'clone'
139    for entry in self.soup.find_all(entry_filter):
140      if entry.name == 'entry':
141        d = {
142              'name': fully_qualified_name(entry),
143              'type': entry['type'],
144              'kind': find_kind(entry),
145              'type_notes': entry.attrs.get('type_notes')
146            }
147
148        d2 = self._parse_entry(entry)
149        insert = self.metadata.insert_entry
150      else:
151        d = {
152           'name': entry['entry'],
153           'kind': find_kind(entry),
154           'target_kind': entry['kind'],
155          # no type since its the same
156          # no type_notes since its the same
157        }
158        d2 = {}
159
160        insert = self.metadata.insert_clone
161
162      d3 = self._parse_entry_optional(entry)
163
164      entry_dict = dict(d.items() + d2.items() + d3.items())
165      insert(entry_dict)
166
167    self.metadata.construct_graph()
168
169  def _parse_entry(self, entry):
170    d = {}
171
172    #
173    # Visibility
174    #
175    d['visibility'] = entry.get('visibility')
176
177    #
178    # Synthetic ?
179    #
180    d['synthetic'] = entry.get('synthetic') == 'true'
181
182    #
183    # Hardware Level (one of limited, legacy, full)
184    #
185    d['hwlevel'] = entry.get('hwlevel')
186
187    #
188    # Deprecated ?
189    #
190    d['deprecated'] = entry.get('deprecated') == 'true'
191
192    #
193    # Optional for non-full hardware level devices
194    #
195    d['optional'] = entry.get('optional') == 'true'
196
197    #
198    # Typedef
199    #
200    d['type_name'] = entry.get('typedef')
201
202    #
203    # Enum
204    #
205    if entry.get('enum', 'false') == 'true':
206
207      enum_values = []
208      enum_deprecateds = []
209      enum_optionals = []
210      enum_hiddens = []
211      enum_ndk_hiddens = []
212      enum_notes = {}
213      enum_ids = {}
214      for value in entry.enum.find_all('value'):
215
216        value_body = self._strings_no_nl(value)
217        enum_values.append(value_body)
218
219        if value.attrs.get('deprecated', 'false') == 'true':
220          enum_deprecateds.append(value_body)
221
222        if value.attrs.get('optional', 'false') == 'true':
223          enum_optionals.append(value_body)
224
225        if value.attrs.get('hidden', 'false') == 'true':
226          enum_hiddens.append(value_body)
227
228        if value.attrs.get('ndk_hidden', 'false') == 'true':
229          enum_ndk_hiddens.append(value_body)
230
231        notes = value.find('notes')
232        if notes is not None:
233          enum_notes[value_body] = notes.string
234
235        if value.attrs.get('id') is not None:
236          enum_ids[value_body] = value['id']
237
238      d['enum_values'] = enum_values
239      d['enum_deprecateds'] = enum_deprecateds
240      d['enum_optionals'] = enum_optionals
241      d['enum_hiddens'] = enum_hiddens
242      d['enum_ndk_hiddens'] = enum_ndk_hiddens
243      d['enum_notes'] = enum_notes
244      d['enum_ids'] = enum_ids
245      d['enum'] = True
246
247    #
248    # Container (Array/Tuple)
249    #
250    if entry.attrs.get('container') is not None:
251      container_name = entry['container']
252
253      array = entry.find('array')
254      if array is not None:
255        array_sizes = []
256        for size in array.find_all('size'):
257          array_sizes.append(size.string)
258        d['container_sizes'] = array_sizes
259
260      tupl = entry.find('tuple')
261      if tupl is not None:
262        tupl_values = []
263        for val in tupl.find_all('value'):
264          tupl_values.append(val.name)
265        d['tuple_values'] = tupl_values
266        d['container_sizes'] = len(tupl_values)
267
268      d['container'] = container_name
269
270    return d
271
272  def _parse_entry_optional(self, entry):
273    d = {}
274
275    optional_elements = ['description', 'range', 'units', 'details', 'hal_details']
276    for i in optional_elements:
277      prop = find_child_tag(entry, i)
278
279      if prop is not None:
280        d[i] = prop.string
281
282    tag_ids = []
283    for tag in entry.find_all('tag'):
284      tag_ids.append(tag['id'])
285
286    d['tag_ids'] = tag_ids
287
288    return d
289
290  def render(self, template, output_name=None):
291    """
292    Render the metadata model using a Mako template as the view.
293
294    The template gets the metadata as an argument, as well as all
295    public attributes from the metadata_helpers module.
296
297    The output file is encoded with UTF-8.
298
299    Args:
300      template: path to a Mako template file
301      output_name: path to the output file, or None to use stdout
302    """
303    buf = StringIO.StringIO()
304    metadata_helpers._context_buf = buf
305
306    helpers = [(i, getattr(metadata_helpers, i))
307                for i in dir(metadata_helpers) if not i.startswith('_')]
308    helpers = dict(helpers)
309
310    lookup = TemplateLookup(directories=[os.getcwd()])
311    tpl = Template(filename=template, lookup=lookup)
312
313    ctx = Context(buf, metadata=self.metadata, **helpers)
314    tpl.render_context(ctx)
315
316    tpl_data = buf.getvalue()
317    metadata_helpers._context_buf = None
318    buf.close()
319
320    if output_name is None:
321      print tpl_data
322    else:
323      file(output_name, "w").write(tpl_data.encode('utf-8'))
324
325#####################
326#####################
327
328if __name__ == "__main__":
329  if len(sys.argv) <= 2:
330    print >> sys.stderr,                                                       \
331           "Usage: %s <filename.xml> <template.mako> [<output_file>]"          \
332           % (sys.argv[0])
333    sys.exit(0)
334
335  file_name = sys.argv[1]
336  template_name = sys.argv[2]
337  output_name = sys.argv[3] if len(sys.argv) > 3 else None
338  parser = MetadataParserXml.create_from_file(file_name)
339  parser.render(template_name, output_name)
340
341  sys.exit(0)
342