4 # Copyright (C) 2012 The Android Open Source Project
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
10 # http://www.apache.org/licenses/LICENSE-2.0
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.
21 metadata_validate.py <filename.xml>
22 - validates that the metadata properties defined in filename.xml are
24 - does not do any XSD validation, use xmllint for that (in metadata-validate)
27 A set of helpful functions for dealing with BeautifulSoup element trees.
28 Especially the find_* and fully_qualified_name functions.
31 BeautifulSoup - an HTML/XML parser available to download from
32 http://www.crummy.com/software/BeautifulSoup/
35 from bs4 import BeautifulSoup
43 def fully_qualified_name(entry):
45 Calculates the fully qualified name for an entry by walking the path
49 entry: a BeautifulSoup Tag corresponding to an <entry ...> XML node,
50 or a <clone ...> XML node.
53 ValueError: if entry does not correspond to one of the above XML nodes
56 A string with the full name, e.g. "android.lens.info.availableApertureSizes"
59 filter_tags = ['namespace', 'section']
60 parents = [i['name'] for i in entry.parents if i.name in filter_tags]
62 if entry.name == 'entry':
64 elif entry.name == 'clone':
65 name = entry['entry'].split(".")[-1] # "a.b.c" => "c"
67 raise ValueError("Unsupported tag type '%s' for element '%s'" \
73 fqn = ".".join(parents)
77 def find_parent_by_name(element, names):
79 Find the ancestor for an element whose name matches one of those
83 element: A BeautifulSoup Tag corresponding to an XML node
86 A BeautifulSoup element corresponding to the matched parent, or None.
88 For example, assuming the following XML structure:
91 <entry name="Hello" /> # this is in variable 'Hello'
95 el = find_parent_by_name(Hello, ['static'])
96 # el is now a value pointing to the '<static>' element
98 matching_parents = [i.name for i in element.parents if i.name in names]
101 return matching_parents[0]
105 def find_all_child_tags(element, tag):
107 Finds all the children that are a Tag (as opposed to a NavigableString),
108 with a name of tag. This is useful to filter out the NavigableString out
112 element: A BeautifulSoup Tag corresponding to an XML node
113 tag: A string representing the name of the tag
116 A list of Tag instances
118 For example, given the following XML structure:
119 <enum> # This is the variable el
120 Hello world # NavigableString
121 <value>Apple</value> # this is the variale apple (Tag)
122 <value>Orange</value> # this is the variable orange (Tag)
123 Hello world again # NavigableString
126 lst = find_all_child_tags(el, 'value')
127 # lst is [apple, orange]
130 matching_tags = [i for i in element.children if isinstance(i, Tag) and i.name == tag]
133 def find_child_tag(element, tag):
135 Finds the first child that is a Tag with the matching name.
138 element: a BeautifulSoup Tag
139 tag: A String representing the name of the tag
142 An instance of a Tag, or None if there was no matches.
144 For example, given the following XML structure:
145 <enum> # This is the variable el
146 Hello world # NavigableString
147 <value>Apple</value> # this is the variale apple (Tag)
148 <value>Orange</value> # this is the variable orange (Tag)
149 Hello world again # NavigableString
152 res = find_child_tag(el, 'value')
155 matching_tags = find_all_child_tags(element, tag)
157 return matching_tags[0]
161 def find_kind(element):
163 Finds the kind Tag ancestor for an element.
166 element: a BeautifulSoup Tag
169 a BeautifulSoup tag, or None if there was no matches
172 This function only makes sense to be called for an Entry, Clone, or
173 InnerNamespace XML types. It will always return 'None' for other nodes.
175 kinds = ['dynamic', 'static', 'controls']
176 parent_kind = find_parent_by_name(element, kinds)
179 def validate_error(msg):
181 Print a validation error to stderr.
184 msg: a string you want to be printed
186 print >> sys.stderr, "ERROR: " + msg
189 def validate_clones(soup):
191 Validate that all <clone> elements point to an existing <entry> element.
194 soup - an instance of BeautifulSoup
197 True if the validation succeeds, False otherwise
201 for clone in soup.find_all("clone"):
202 clone_entry = clone['entry']
203 clone_kind = clone['kind']
205 parent_kind = find_kind(clone)
207 find_entry = lambda x: x.name == 'entry' \
208 and find_kind(x) == clone_kind \
209 and fully_qualified_name(x) == clone_entry
210 matching_entry = soup.find(find_entry)
212 if matching_entry is None:
213 error_msg = ("Did not find corresponding clone entry '%s' " + \
214 "with kind '%s'") %(clone_entry, clone_kind)
215 validate_error(error_msg)
218 clone_name = fully_qualified_name(clone)
219 if clone_name != clone_entry:
220 error_msg = ("Clone entry target '%s' did not match fully qualified " + \
221 "name '%s'.") %(clone_entry, clone_name)
222 validate_error(error_msg)
225 if matching_entry is not None:
226 entry_hal_major_version = 3
227 entry_hal_minor_version = 2
228 entry_hal_version = matching_entry.get('hal_version')
229 if entry_hal_version is not None:
230 entry_hal_major_version = int(entry_hal_version.partition('.')[0])
231 entry_hal_minor_version = int(entry_hal_version.partition('.')[2])
233 clone_hal_major_version = entry_hal_major_version
234 clone_hal_minor_version = entry_hal_minor_version
235 clone_hal_version = clone.get('hal_version')
236 if clone_hal_version is not None:
237 clone_hal_major_version = int(clone_hal_version.partition('.')[0])
238 clone_hal_minor_version = int(clone_hal_version.partition('.')[2])
240 if clone_hal_major_version < entry_hal_major_version or \
241 (clone_hal_major_version == entry_hal_major_version and \
242 clone_hal_minor_version < entry_hal_minor_version):
243 error_msg = ("Clone '%s' HAL version '%d.%d' is older than entry target HAL version '%d.%d'" \
244 % (clone_name, clone_hal_major_version, clone_hal_minor_version, entry_hal_major_version, entry_hal_minor_version))
245 validate_error(error_msg)
250 # All <entry> elements with container=$foo have a <$foo> child
251 # If type="enum", <enum> tag is present
252 # In <enum> for all <value id="$x">, $x is numeric
253 def validate_entries(soup):
255 Validate all <entry> elements with the following rules:
256 * If there is a container="$foo" attribute, there is a <$foo> child
257 * If there is a type="enum" attribute, there is an <enum> child
258 * In the <enum> child, all <value id="$x"> have a numeric $x
261 soup - an instance of BeautifulSoup
264 True if the validation succeeds, False otherwise
267 for entry in soup.find_all("entry"):
268 entry_container = entry.attrs.get('container')
270 if entry_container is not None:
271 container_tag = entry.find(entry_container)
273 if container_tag is None:
275 validate_error(("Entry '%s' in kind '%s' has type '%s' but " + \
276 "missing child element <%s>") \
277 %(fully_qualified_name(entry), find_kind(entry), \
278 entry_container, entry_container))
280 enum = entry.attrs.get('enum')
281 if enum and enum == 'true':
282 if entry.enum is None:
283 validate_error(("Entry '%s' in kind '%s' is missing enum") \
284 % (fully_qualified_name(entry), find_kind(entry),
289 for value in entry.enum.find_all('value'):
290 value_id = value.attrs.get('id')
292 if value_id is not None:
294 id_int = int(value_id, 0) #autoguess base
296 validate_error(("Entry '%s' has id '%s', which is not" + \
298 %(fully_qualified_name(entry), value_id))
302 validate_error(("Entry '%s' kind '%s' has enum el, but no enum attr") \
303 % (fully_qualified_name(entry), find_kind(entry),
307 deprecated = entry.attrs.get('deprecated')
308 if deprecated and deprecated == 'true':
309 if entry.deprecation_description is None:
310 validate_error(("Entry '%s' in kind '%s' is deprecated, but missing deprecation description") \
311 % (fully_qualified_name(entry), find_kind(entry),
315 if entry.deprecation_description is not None:
316 validate_error(("Entry '%s' in kind '%s' has deprecation description, but is not deprecated") \
317 % (fully_qualified_name(entry), find_kind(entry),
323 def validate_xml(xml):
325 Validate all XML nodes according to the rules in validate_clones and
329 xml - A string containing a block of XML to validate
332 a BeautifulSoup instance if validation succeeds, None otherwise
335 soup = BeautifulSoup(xml, features='xml')
337 succ = validate_clones(soup)
338 succ = validate_entries(soup) and succ
345 #####################
346 #####################
348 if __name__ == "__main__":
349 if len(sys.argv) <= 1:
350 print >> sys.stderr, "Usage: %s <filename.xml>" % (sys.argv[0])
353 file_name = sys.argv[1]
354 succ = validate_xml(file(file_name).read()) is not None
357 print "%s: SUCCESS! Document validated" %(file_name)
360 print >> sys.stderr, "%s: ERRORS: Document failed to validate" %(file_name)