1#!/usr/bin/env python3
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
40
41from bs4 import BeautifulSoup
42from bs4 import NavigableString
43
44from datetime import datetime
45
46from io import StringIO
47
48from mako.template import Template
49from mako.lookup import TemplateLookup
50from mako.runtime import Context
51
52from metadata_model import *
53import metadata_model
54from metadata_validate import *
55import metadata_helpers
56
57class MetadataParserXml:
58  """
59  A class to parse any XML block that passes validation with metadata-validate.
60  It builds a metadata_model.Metadata graph and then renders it over a
61  Mako template.
62
63  Attributes (Read-Only):
64    soup: an instance of BeautifulSoup corresponding to the XML contents
65    metadata: a constructed instance of metadata_model.Metadata
66  """
67  def __init__(self, xml, file_name):
68    """
69    Construct a new MetadataParserXml, immediately try to parse it into a
70    metadata model.
71
72    Args:
73      xml: The XML block to use for the metadata
74      file_name: Source of the XML block, only for debugging/errors
75
76    Raises:
77      ValueError: if the XML block failed to pass metadata_validate.py
78    """
79    self._soup = validate_xml(xml)
80
81    if self._soup is None:
82      raise ValueError("%s has an invalid XML file" % (file_name))
83
84    self._metadata = Metadata()
85    self._parse()
86    self._metadata.construct_graph()
87
88  @staticmethod
89  def create_from_file(file_name):
90    """
91    Construct a new MetadataParserXml by loading and parsing an XML file.
92
93    Args:
94      file_name: Name of the XML file to load and parse.
95
96    Raises:
97      ValueError: if the XML file failed to pass metadata_validate.py
98
99    Returns:
100      MetadataParserXml instance representing the XML file.
101    """
102    return MetadataParserXml(open(file_name).read(), file_name)
103
104  @property
105  def soup(self):
106    return self._soup
107
108  @property
109  def metadata(self):
110    return self._metadata
111
112  @staticmethod
113  def _find_direct_strings(element):
114    if element.string is not None:
115      return [element.string]
116
117    return [i for i in element.contents if isinstance(i, NavigableString)]
118
119  @staticmethod
120  def _strings_no_nl(element):
121    return "".join([i.strip() for i in MetadataParserXml._find_direct_strings(element)])
122
123  def _parse(self):
124
125    tags = self.soup.tags
126    if tags is not None:
127      for tag in tags.find_all('tag'):
128        self.metadata.insert_tag(tag['id'], tag.string)
129
130    types = self.soup.types
131    if types is not None:
132      for tp in types.find_all('typedef'):
133        languages = {}
134        for lang in tp.find_all('language'):
135          languages[lang['name']] = lang.string
136
137        self.metadata.insert_type(tp['name'], 'typedef', languages=languages)
138
139    # add all entries, preserving the ordering of the XML file
140    # this is important for future ABI compatibility when generating code
141    entry_filter = lambda x: x.name == 'entry' or x.name == 'clone'
142    for entry in self.soup.find_all(entry_filter):
143      if entry.name == 'entry':
144        d = {
145              'name': fully_qualified_name(entry),
146              'type': entry['type'],
147              'kind': find_kind(entry),
148              'type_notes': entry.attrs.get('type_notes')
149            }
150
151        d2 = self._parse_entry(entry)
152        insert = self.metadata.insert_entry
153      else:
154        d = {
155           'name': entry['entry'],
156           'kind': find_kind(entry),
157           'target_kind': entry['kind'],
158          # no type since its the same
159          # no type_notes since its the same
160        }
161        d2 = {}
162        if 'hal_version' in entry.attrs:
163          d2['hal_version'] = entry['hal_version']
164
165        insert = self.metadata.insert_clone
166
167      d3 = self._parse_entry_optional(entry)
168
169      entry_dict = {**d, **d2, **d3}
170      insert(entry_dict)
171
172    self.metadata.construct_graph()
173
174  def _parse_entry(self, entry):
175    d = {}
176
177    #
178    # Visibility
179    #
180    d['visibility'] = entry.get('visibility')
181
182    #
183    # Synthetic ?
184    #
185    d['synthetic'] = entry.get('synthetic') == 'true'
186
187    #
188    # Permission needed ?
189    #
190    d['permission_needed'] = entry.get('permission_needed')
191
192    # Aconfig flag gating this entry ?
193    d['aconfig_flag'] = entry.get('aconfig_flag')
194
195    #
196    # Hardware Level (one of limited, legacy, full)
197    #
198    d['hwlevel'] = entry.get('hwlevel')
199
200    #
201    # Deprecated ?
202    #
203    d['deprecated'] = entry.get('deprecated') == 'true'
204
205    #
206    # Optional for non-full hardware level devices
207    #
208    d['optional'] = entry.get('optional') == 'true'
209
210    #
211    # Typedef
212    #
213    d['type_name'] = entry.get('typedef')
214
215    #
216    # Initial HIDL HAL version the entry was added in
217    d['hal_version'] = entry.get('hal_version')
218
219    #
220    # HAL version from which this entry became a session characteristic ?
221    d['session_characteristics_key_since'] = entry.get('session_characteristics_key_since')
222
223    #
224    # Enum
225    #
226    if entry.get('enum', 'false') == 'true':
227
228      enum_values = []
229      enum_deprecateds = []
230      enum_optionals = []
231      enum_visibilities = {}
232      enum_notes = {}
233      enum_sdk_notes = {}
234      enum_ndk_notes = {}
235      enum_ids = {}
236      enum_hal_versions = {}
237      enum_aconfig_flags = {}
238      for value in entry.enum.find_all('value'):
239
240        value_body = self._strings_no_nl(value)
241        enum_values.append(value_body)
242
243        if value.attrs.get('deprecated', 'false') == 'true':
244          enum_deprecateds.append(value_body)
245
246        if value.attrs.get('optional', 'false') == 'true':
247          enum_optionals.append(value_body)
248
249        visibility = value.attrs.get('visibility')
250        if visibility is not None:
251          enum_visibilities[value_body] = visibility
252
253        notes = value.find('notes')
254        if notes is not None:
255          enum_notes[value_body] = notes.string
256
257        sdk_notes = value.find('sdk_notes')
258        if sdk_notes is not None:
259          enum_sdk_notes[value_body] = sdk_notes.string
260
261        ndk_notes = value.find('ndk_notes')
262        if ndk_notes is not None:
263          enum_ndk_notes[value_body] = ndk_notes.string
264
265        if value.attrs.get('id') is not None:
266          enum_ids[value_body] = value['id']
267
268        if value.attrs.get('hal_version') is not None:
269          enum_hal_versions[value_body] = value['hal_version']
270
271        if value.attrs.get('aconfig_flag') is not None:
272          enum_aconfig_flags[value_body] = value['aconfig_flag']
273
274      d['enum_values'] = enum_values
275      d['enum_deprecateds'] = enum_deprecateds
276      d['enum_optionals'] = enum_optionals
277      d['enum_visibilities'] = enum_visibilities
278      d['enum_notes'] = enum_notes
279      d['enum_sdk_notes'] = enum_sdk_notes
280      d['enum_ndk_notes'] = enum_ndk_notes
281      d['enum_ids'] = enum_ids
282      d['enum_hal_versions'] = enum_hal_versions
283      d['enum_aconfig_flags'] = enum_aconfig_flags
284      d['enum'] = True
285
286    #
287    # Container (Array/Tuple)
288    #
289    if entry.attrs.get('container') is not None:
290      container_name = entry['container']
291
292      array = entry.find('array')
293      if array is not None:
294        array_sizes = []
295        for size in array.find_all('size'):
296          array_sizes.append(size.string)
297        d['container_sizes'] = array_sizes
298
299      tupl = entry.find('tuple')
300      if tupl is not None:
301        tupl_values = []
302        for val in tupl.find_all('value'):
303          tupl_values.append(val.name)
304        d['tuple_values'] = tupl_values
305        d['container_sizes'] = len(tupl_values)
306
307      d['container'] = container_name
308
309    return d
310
311  def _parse_entry_optional(self, entry):
312    d = {}
313
314    optional_elements = ['description', 'range', 'units', 'details', 'hal_details', 'ndk_details',\
315                         'deprecation_description']
316    for i in optional_elements:
317      prop = find_child_tag(entry, i)
318
319      if prop is not None:
320        d[i] = prop.string
321
322    tag_ids = []
323    for tag in entry.find_all('tag'):
324      tag_ids.append(tag['id'])
325
326    d['tag_ids'] = tag_ids
327
328    return d
329
330  def render(self, template, output_name=None, enum=None,
331             copyright_year=None):
332    """
333    Render the metadata model using a Mako template as the view.
334
335    The template gets the metadata as an argument, as well as all
336    public attributes from the metadata_helpers module.
337
338    The output file is encoded with UTF-8.
339
340    Args:
341      template: path to a Mako template file
342      output_name: path to the output file, or None to use stdout
343      enum: The name of the enum, if any
344      copyright_year: the year in the copyright section of output file
345    """
346    buf = StringIO()
347    metadata_helpers._context_buf = buf
348    metadata_helpers._enum = enum
349
350    copyright_year = copyright_year \
351                        if copyright_year is not None \
352                        else str(datetime.now().year)
353    metadata_helpers._copyright_year = \
354        metadata_helpers.infer_copyright_year_from_source(output_name,
355                                                          copyright_year)
356
357    helpers = [(i, getattr(metadata_helpers, i))
358                for i in dir(metadata_helpers) if not i.startswith('_')]
359    helpers = dict(helpers)
360
361    lookup = TemplateLookup(directories=[os.getcwd()])
362    tpl = Template(filename=template, lookup=lookup)
363
364    ctx = Context(buf, metadata=self.metadata, **helpers)
365    tpl.render_context(ctx)
366
367    tpl_data = buf.getvalue()
368    metadata_helpers._context_buf = None
369    buf.close()
370
371    if output_name is None:
372      print(tpl_data)
373    else:
374      open(output_name, "w").write(tpl_data)
375
376#####################
377#####################
378
379if __name__ == "__main__":
380  if len(sys.argv) <= 2:
381    print("Usage: %s <filename.xml> <template.mako> [<output_file>]"\
382          " [<copyright_year>]" \
383          % (sys.argv[0]), file=sys.stderr)
384    sys.exit(0)
385
386  file_name = sys.argv[1]
387  template_name = sys.argv[2]
388  output_name = sys.argv[3] if len(sys.argv) > 3 else None
389  copyright_year = sys.argv[4] if len(sys.argv) > 4 else str(datetime.now().year)
390
391  parser = MetadataParserXml.create_from_file(file_name)
392  parser.render(template_name, output_name, None, copyright_year)
393
394  sys.exit(0)
395