Home | History | Annotate | Download | only in docs
      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     # add all entries, preserving the ordering of the XML file
    111     # this is important for future ABI compatibility when generating code
    112     entry_filter = lambda x: x.name == 'entry' or x.name == 'clone'
    113     for entry in self.soup.find_all(entry_filter):
    114       if entry.name == 'entry':
    115         d = {
    116               'name': fully_qualified_name(entry),
    117               'type': entry['type'],
    118               'kind': find_kind(entry),
    119               'type_notes': entry.attrs.get('type_notes')
    120             }
    121 
    122         d2 = self._parse_entry(entry)
    123         insert = self.metadata.insert_entry
    124       else:
    125         d = {
    126            'name': entry['entry'],
    127            'kind': find_kind(entry),
    128            'target_kind': entry['kind'],
    129           # no type since its the same
    130           # no type_notes since its the same
    131         }
    132         d2 = {}
    133 
    134         insert = self.metadata.insert_clone
    135 
    136       d3 = self._parse_entry_optional(entry)
    137 
    138       entry_dict = dict(d.items() + d2.items() + d3.items())
    139       insert(entry_dict)
    140 
    141     self.metadata.construct_graph()
    142 
    143   def _parse_entry(self, entry):
    144     d = {}
    145 
    146     #
    147     # Enum
    148     #
    149     if entry.get('enum', 'false') == 'true':
    150 
    151       enum_values = []
    152       enum_optionals = []
    153       enum_notes = {}
    154       enum_ids = {}
    155       for value in entry.enum.find_all('value'):
    156 
    157         value_body = self._strings_no_nl(value)
    158         enum_values.append(value_body)
    159 
    160         if value.attrs.get('optional', 'false') == 'true':
    161           enum_optionals.append(value_body)
    162 
    163         notes = value.find('notes')
    164         if notes is not None:
    165           enum_notes[value_body] = notes.string
    166 
    167         if value.attrs.get('id') is not None:
    168           enum_ids[value_body] = value['id']
    169 
    170       d['enum_values'] = enum_values
    171       d['enum_optionals'] = enum_optionals
    172       d['enum_notes'] = enum_notes
    173       d['enum_ids'] = enum_ids
    174       d['enum'] = True
    175 
    176     #
    177     # Container (Array/Tuple)
    178     #
    179     if entry.attrs.get('container') is not None:
    180       container_name = entry['container']
    181 
    182       array = entry.find('array')
    183       if array is not None:
    184         array_sizes = []
    185         for size in array.find_all('size'):
    186           array_sizes.append(size.string)
    187         d['container_sizes'] = array_sizes
    188 
    189       tupl = entry.find('tuple')
    190       if tupl is not None:
    191         tupl_values = []
    192         for val in tupl.find_all('value'):
    193           tupl_values.append(val.name)
    194         d['tuple_values'] = tupl_values
    195         d['container_sizes'] = len(tupl_values)
    196 
    197       d['container'] = container_name
    198 
    199     return d
    200 
    201   def _parse_entry_optional(self, entry):
    202     d = {}
    203 
    204     optional_elements = ['description', 'range', 'units', 'notes']
    205     for i in optional_elements:
    206       prop = find_child_tag(entry, i)
    207 
    208       if prop is not None:
    209         d[i] = prop.string
    210 
    211     tag_ids = []
    212     for tag in entry.find_all('tag'):
    213       tag_ids.append(tag['id'])
    214 
    215     d['tag_ids'] = tag_ids
    216 
    217     return d
    218 
    219   def render(self, template, output_name=None):
    220     """
    221     Render the metadata model using a Mako template as the view.
    222 
    223     The template gets the metadata as an argument, as well as all
    224     public attributes from the metadata_helpers module.
    225 
    226     Args:
    227       template: path to a Mako template file
    228       output_name: path to the output file, or None to use stdout
    229     """
    230     buf = StringIO.StringIO()
    231     metadata_helpers._context_buf = buf
    232 
    233     helpers = [(i, getattr(metadata_helpers, i))
    234                 for i in dir(metadata_helpers) if not i.startswith('_')]
    235     helpers = dict(helpers)
    236 
    237     lookup = TemplateLookup(directories=[os.getcwd()])
    238     tpl = Template(filename=template, lookup=lookup)
    239 
    240     ctx = Context(buf, metadata=self.metadata, **helpers)
    241     tpl.render_context(ctx)
    242 
    243     tpl_data = buf.getvalue()
    244     metadata_helpers._context_buf = None
    245     buf.close()
    246 
    247     if output_name is None:
    248       print tpl_data
    249     else:
    250       file(output_name, "w").write(tpl_data)
    251 
    252 #####################
    253 #####################
    254 
    255 if __name__ == "__main__":
    256   if len(sys.argv) <= 2:
    257     print >> sys.stderr,                                                       \
    258            "Usage: %s <filename.xml> <template.mako> [<output_file>]"          \
    259            % (sys.argv[0])
    260     sys.exit(0)
    261 
    262   file_name = sys.argv[1]
    263   template_name = sys.argv[2]
    264   output_name = sys.argv[3] if len(sys.argv) > 3 else None
    265   parser = MetadataParserXml(file_name)
    266   parser.render(template_name, output_name)
    267 
    268   sys.exit(0)
    269