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_definitions.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        if 'hal_version' in entry.attrs:
160          d2['hal_version'] = entry['hal_version']
161
162        insert = self.metadata.insert_clone
163
164      d3 = self._parse_entry_optional(entry)
165
166      entry_dict = dict(d.items() + d2.items() + d3.items())
167      insert(entry_dict)
168
169    self.metadata.construct_graph()
170
171  def _parse_entry(self, entry):
172    d = {}
173
174    #
175    # Visibility
176    #
177    d['visibility'] = entry.get('visibility')
178
179    #
180    # Synthetic ?
181    #
182    d['synthetic'] = entry.get('synthetic') == 'true'
183
184    #
185    # Permission needed ?
186    #
187    d['permission_needed'] = entry.get('permission_needed')
188
189    #
190    # Hardware Level (one of limited, legacy, full)
191    #
192    d['hwlevel'] = entry.get('hwlevel')
193
194    #
195    # Deprecated ?
196    #
197    d['deprecated'] = entry.get('deprecated') == 'true'
198
199    #
200    # Optional for non-full hardware level devices
201    #
202    d['optional'] = entry.get('optional') == 'true'
203
204    #
205    # Typedef
206    #
207    d['type_name'] = entry.get('typedef')
208
209    #
210    # Initial HIDL HAL version the entry was added in
211    d['hal_version'] = entry.get('hal_version')
212
213    #
214    # Enum
215    #
216    if entry.get('enum', 'false') == 'true':
217
218      enum_values = []
219      enum_deprecateds = []
220      enum_optionals = []
221      enum_hiddens = []
222      enum_ndk_hiddens = []
223      enum_notes = {}
224      enum_sdk_notes = {}
225      enum_ndk_notes = {}
226      enum_ids = {}
227      enum_hal_versions = {}
228      for value in entry.enum.find_all('value'):
229
230        value_body = self._strings_no_nl(value)
231        enum_values.append(value_body)
232
233        if value.attrs.get('deprecated', 'false') == 'true':
234          enum_deprecateds.append(value_body)
235
236        if value.attrs.get('optional', 'false') == 'true':
237          enum_optionals.append(value_body)
238
239        if value.attrs.get('hidden', 'false') == 'true':
240          enum_hiddens.append(value_body)
241
242        if value.attrs.get('ndk_hidden', 'false') == 'true':
243          enum_ndk_hiddens.append(value_body)
244
245        notes = value.find('notes')
246        if notes is not None:
247          enum_notes[value_body] = notes.string
248
249        sdk_notes = value.find('sdk_notes')
250        if sdk_notes is not None:
251          enum_sdk_notes[value_body] = sdk_notes.string
252
253        ndk_notes = value.find('ndk_notes')
254        if ndk_notes is not None:
255          enum_ndk_notes[value_body] = ndk_notes.string
256
257        if value.attrs.get('id') is not None:
258          enum_ids[value_body] = value['id']
259
260        if value.attrs.get('hal_version') is not None:
261          enum_hal_versions[value_body] = value['hal_version']
262
263      d['enum_values'] = enum_values
264      d['enum_deprecateds'] = enum_deprecateds
265      d['enum_optionals'] = enum_optionals
266      d['enum_hiddens'] = enum_hiddens
267      d['enum_ndk_hiddens'] = enum_ndk_hiddens
268      d['enum_notes'] = enum_notes
269      d['enum_sdk_notes'] = enum_sdk_notes
270      d['enum_ndk_notes'] = enum_ndk_notes
271      d['enum_ids'] = enum_ids
272      d['enum_hal_versions'] = enum_hal_versions
273      d['enum'] = True
274
275    #
276    # Container (Array/Tuple)
277    #
278    if entry.attrs.get('container') is not None:
279      container_name = entry['container']
280
281      array = entry.find('array')
282      if array is not None:
283        array_sizes = []
284        for size in array.find_all('size'):
285          array_sizes.append(size.string)
286        d['container_sizes'] = array_sizes
287
288      tupl = entry.find('tuple')
289      if tupl is not None:
290        tupl_values = []
291        for val in tupl.find_all('value'):
292          tupl_values.append(val.name)
293        d['tuple_values'] = tupl_values
294        d['container_sizes'] = len(tupl_values)
295
296      d['container'] = container_name
297
298    return d
299
300  def _parse_entry_optional(self, entry):
301    d = {}
302
303    optional_elements = ['description', 'range', 'units', 'details', 'hal_details', 'ndk_details',\
304                         'deprecation_description']
305    for i in optional_elements:
306      prop = find_child_tag(entry, i)
307
308      if prop is not None:
309        d[i] = prop.string
310
311    tag_ids = []
312    for tag in entry.find_all('tag'):
313      tag_ids.append(tag['id'])
314
315    d['tag_ids'] = tag_ids
316
317    return d
318
319  def render(self, template, output_name=None, hal_version="3.2"):
320    """
321    Render the metadata model using a Mako template as the view.
322
323    The template gets the metadata as an argument, as well as all
324    public attributes from the metadata_helpers module.
325
326    The output file is encoded with UTF-8.
327
328    Args:
329      template: path to a Mako template file
330      output_name: path to the output file, or None to use stdout
331      hal_version: target HAL version, used when generating HIDL HAL outputs.
332                   Must be a string of form "X.Y" where X and Y are integers.
333    """
334    buf = StringIO.StringIO()
335    metadata_helpers._context_buf = buf
336    metadata_helpers._hal_major_version = int(hal_version.partition('.')[0])
337    metadata_helpers._hal_minor_version = int(hal_version.partition('.')[2])
338
339    helpers = [(i, getattr(metadata_helpers, i))
340                for i in dir(metadata_helpers) if not i.startswith('_')]
341    helpers = dict(helpers)
342
343    lookup = TemplateLookup(directories=[os.getcwd()])
344    tpl = Template(filename=template, lookup=lookup)
345
346    ctx = Context(buf, metadata=self.metadata, **helpers)
347    tpl.render_context(ctx)
348
349    tpl_data = buf.getvalue()
350    metadata_helpers._context_buf = None
351    buf.close()
352
353    if output_name is None:
354      print tpl_data
355    else:
356      file(output_name, "w").write(tpl_data.encode('utf-8'))
357
358#####################
359#####################
360
361if __name__ == "__main__":
362  if len(sys.argv) <= 2:
363    print >> sys.stderr,                                                       \
364           "Usage: %s <filename.xml> <template.mako> [<output_file>] [<hal_version>]"          \
365           % (sys.argv[0])
366    sys.exit(0)
367
368  file_name = sys.argv[1]
369  template_name = sys.argv[2]
370  output_name = sys.argv[3] if len(sys.argv) > 3 else None
371  hal_version = sys.argv[4] if len(sys.argv) > 4 else "3.2"
372
373  parser = MetadataParserXml.create_from_file(file_name)
374  parser.render(template_name, output_name, hal_version)
375
376  sys.exit(0)
377