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