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"""
20A parser for metadata_properties.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
40import StringIO
41
42from bs4 import BeautifulSoup
43from bs4 import NavigableString
44
45from mako.template import Template
46from mako.lookup import TemplateLookup
47from mako.runtime import Context
48
49from metadata_model import *
50import metadata_model
51from metadata_validate import *
52import metadata_helpers
53
54class 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
255if __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