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 """ 20 A parser for metadata_properties.xml can also render the resulting model 21 over a Mako template. 22 23 Usage: 24 metadata_parser_xml.py <filename.xml> <template.mako> [<output_file>] 25 - outputs the resulting template to output_file (stdout if none specified) 26 27 Module: 28 The parser is also available as a module import (MetadataParserXml) to use 29 in other modules. 30 31 Dependencies: 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 38 import sys 39 import os 40 import StringIO 41 42 from bs4 import BeautifulSoup 43 from bs4 import NavigableString 44 45 from mako.template import Template 46 from mako.lookup import TemplateLookup 47 from mako.runtime import Context 48 49 from metadata_model import * 50 import metadata_model 51 from metadata_validate import * 52 import metadata_helpers 53 54 class MetadataParserXml: 55 """ 56 A class to parse any XML file 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, file_name): 65 """ 66 Construct a new MetadataParserXml, immediately try to parse it into a 67 metadata model. 68 69 Args: 70 file_name: path to an XML file that passes metadata-validate 71 72 Raises: 73 ValueError: if the XML file failed to pass metadata_validate.py 74 """ 75 self._soup = validate_xml(file_name) 76 77 if self._soup is None: 78 raise ValueError("%s has an invalid XML file" %(file_name)) 79 80 self._metadata = Metadata() 81 self._parse() 82 self._metadata.construct_graph() 83 84 @property 85 def soup(self): 86 return self._soup 87 88 @property 89 def metadata(self): 90 return self._metadata 91 92 @staticmethod 93 def _find_direct_strings(element): 94 if element.string is not None: 95 return [element.string] 96 97 return [i for i in element.contents if isinstance(i, NavigableString)] 98 99 @staticmethod 100 def _strings_no_nl(element): 101 return "".join([i.strip() for i in MetadataParserXml._find_direct_strings(element)]) 102 103 def _parse(self): 104 105 tags = self.soup.tags 106 if tags is not None: 107 for tag in tags.find_all('tag'): 108 self.metadata.insert_tag(tag['id'], tag.string) 109 110 types = self.soup.types 111 if types is not None: 112 for tp in types.find_all('typedef'): 113 languages = {} 114 for lang in tp.find_all('language'): 115 languages[lang['name']] = lang.string 116 117 self.metadata.insert_type(tp['name'], 'typedef', languages=languages) 118 119 # add all entries, preserving the ordering of the XML file 120 # this is important for future ABI compatibility when generating code 121 entry_filter = lambda x: x.name == 'entry' or x.name == 'clone' 122 for entry in self.soup.find_all(entry_filter): 123 if entry.name == 'entry': 124 d = { 125 'name': fully_qualified_name(entry), 126 'type': entry['type'], 127 'kind': find_kind(entry), 128 'type_notes': entry.attrs.get('type_notes') 129 } 130 131 d2 = self._parse_entry(entry) 132 insert = self.metadata.insert_entry 133 else: 134 d = { 135 'name': entry['entry'], 136 'kind': find_kind(entry), 137 'target_kind': entry['kind'], 138 # no type since its the same 139 # no type_notes since its the same 140 } 141 d2 = {} 142 143 insert = self.metadata.insert_clone 144 145 d3 = self._parse_entry_optional(entry) 146 147 entry_dict = dict(d.items() + d2.items() + d3.items()) 148 insert(entry_dict) 149 150 self.metadata.construct_graph() 151 152 def _parse_entry(self, entry): 153 d = {} 154 155 # 156 # Visibility 157 # 158 d['visibility'] = entry.get('visibility') 159 160 # 161 # Optional for non-full hardware level devices 162 # 163 d['optional'] = entry.get('optional') == 'true' 164 165 # 166 # Typedef 167 # 168 d['type_name'] = entry.get('typedef') 169 170 # 171 # Enum 172 # 173 if entry.get('enum', 'false') == 'true': 174 175 enum_values = [] 176 enum_optionals = [] 177 enum_notes = {} 178 enum_ids = {} 179 for value in entry.enum.find_all('value'): 180 181 value_body = self._strings_no_nl(value) 182 enum_values.append(value_body) 183 184 if value.attrs.get('optional', 'false') == 'true': 185 enum_optionals.append(value_body) 186 187 notes = value.find('notes') 188 if notes is not None: 189 enum_notes[value_body] = notes.string 190 191 if value.attrs.get('id') is not None: 192 enum_ids[value_body] = value['id'] 193 194 d['enum_values'] = enum_values 195 d['enum_optionals'] = enum_optionals 196 d['enum_notes'] = enum_notes 197 d['enum_ids'] = enum_ids 198 d['enum'] = True 199 200 # 201 # Container (Array/Tuple) 202 # 203 if entry.attrs.get('container') is not None: 204 container_name = entry['container'] 205 206 array = entry.find('array') 207 if array is not None: 208 array_sizes = [] 209 for size in array.find_all('size'): 210 array_sizes.append(size.string) 211 d['container_sizes'] = array_sizes 212 213 tupl = entry.find('tuple') 214 if tupl is not None: 215 tupl_values = [] 216 for val in tupl.find_all('value'): 217 tupl_values.append(val.name) 218 d['tuple_values'] = tupl_values 219 d['container_sizes'] = len(tupl_values) 220 221 d['container'] = container_name 222 223 return d 224 225 def _parse_entry_optional(self, entry): 226 d = {} 227 228 optional_elements = ['description', 'range', 'units', 'notes'] 229 for i in optional_elements: 230 prop = find_child_tag(entry, i) 231 232 if prop is not None: 233 d[i] = prop.string 234 235 tag_ids = [] 236 for tag in entry.find_all('tag'): 237 tag_ids.append(tag['id']) 238 239 d['tag_ids'] = tag_ids 240 241 return d 242 243 def render(self, template, output_name=None): 244 """ 245 Render the metadata model using a Mako template as the view. 246 247 The template gets the metadata as an argument, as well as all 248 public attributes from the metadata_helpers module. 249 250 Args: 251 template: path to a Mako template file 252 output_name: path to the output file, or None to use stdout 253 """ 254 buf = StringIO.StringIO() 255 metadata_helpers._context_buf = buf 256 257 helpers = [(i, getattr(metadata_helpers, i)) 258 for i in dir(metadata_helpers) if not i.startswith('_')] 259 helpers = dict(helpers) 260 261 lookup = TemplateLookup(directories=[os.getcwd()]) 262 tpl = Template(filename=template, lookup=lookup) 263 264 ctx = Context(buf, metadata=self.metadata, **helpers) 265 tpl.render_context(ctx) 266 267 tpl_data = buf.getvalue() 268 metadata_helpers._context_buf = None 269 buf.close() 270 271 if output_name is None: 272 print tpl_data 273 else: 274 file(output_name, "w").write(tpl_data) 275 276 ##################### 277 ##################### 278 279 if __name__ == "__main__": 280 if len(sys.argv) <= 2: 281 print >> sys.stderr, \ 282 "Usage: %s <filename.xml> <template.mako> [<output_file>]" \ 283 % (sys.argv[0]) 284 sys.exit(0) 285 286 file_name = sys.argv[1] 287 template_name = sys.argv[2] 288 output_name = sys.argv[3] if len(sys.argv) > 3 else None 289 parser = MetadataParserXml(file_name) 290 parser.render(template_name, output_name) 291 292 sys.exit(0) 293