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