• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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, enum=None, copyright_year="2022"):
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      enum: The name of the enum, if any
329      copyright_year: the year in the copyright section of output file
330    """
331    buf = StringIO()
332    metadata_helpers._context_buf = buf
333    metadata_helpers._enum = enum
334    metadata_helpers._copyright_year = copyright_year
335
336    helpers = [(i, getattr(metadata_helpers, i))
337                for i in dir(metadata_helpers) if not i.startswith('_')]
338    helpers = dict(helpers)
339
340    lookup = TemplateLookup(directories=[os.getcwd()])
341    tpl = Template(filename=template, lookup=lookup)
342
343    ctx = Context(buf, metadata=self.metadata, **helpers)
344    tpl.render_context(ctx)
345
346    tpl_data = buf.getvalue()
347    metadata_helpers._context_buf = None
348    buf.close()
349
350    if output_name is None:
351      print(tpl_data)
352    else:
353      open(output_name, "w").write(tpl_data)
354
355#####################
356#####################
357
358if __name__ == "__main__":
359  if len(sys.argv) <= 2:
360    print("Usage: %s <filename.xml> <template.mako> [<output_file>]"\
361          " [<copyright_year>]" \
362          % (sys.argv[0]), file=sys.stderr)
363    sys.exit(0)
364
365  file_name = sys.argv[1]
366  template_name = sys.argv[2]
367  output_name = sys.argv[3] if len(sys.argv) > 3 else None
368  copyright_year = sys.argv[4] if len(sys.argv) > 4 else "2022"
369
370  parser = MetadataParserXml.create_from_file(file_name)
371  parser.render(template_name, output_name, None, copyright_year)
372
373  sys.exit(0)
374