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)
227 # All <entry> elements with container=$foo have a <$foo> child
228 # If type="enum", <enum> tag is present
229 # In <enum> for all <value id="$x">, $x is numeric
230 def validate_entries(soup):
232 Validate all <entry> elements with the following rules:
233 * If there is a container="$foo" attribute, there is a <$foo> child
234 * If there is a type="enum" attribute, there is an <enum> child
235 * In the <enum> child, all <value id="$x"> have a numeric $x
238 soup - an instance of BeautifulSoup
241 True if the validation succeeds, False otherwise
244 for entry in soup.find_all("entry"):
245 entry_container = entry.attrs.get('container')
247 if entry_container is not None:
248 container_tag = entry.find(entry_container)
250 if container_tag is None:
252 validate_error(("Entry '%s' in kind '%s' has type '%s' but " + \
253 "missing child element <%s>") \
254 %(fully_qualified_name(entry), find_kind(entry), \
255 entry_container, entry_container))
257 enum = entry.attrs.get('enum')
258 if enum and enum == 'true':
259 if entry.enum is None:
260 validate_error(("Entry '%s' in kind '%s' is missing enum") \
261 % (fully_qualified_name(entry), find_kind(entry),
266 for value in entry.enum.find_all('value'):
267 value_id = value.attrs.get('id')
269 if value_id is not None:
271 id_int = int(value_id, 0) #autoguess base
273 validate_error(("Entry '%s' has id '%s', which is not" + \
275 %(fully_qualified_name(entry), value_id))
279 validate_error(("Entry '%s' kind '%s' has enum el, but no enum attr") \
280 % (fully_qualified_name(entry), find_kind(entry),
284 deprecated = entry.attrs.get('deprecated')
285 if deprecated and deprecated == 'true':
286 if entry.deprecation_description is None:
287 validate_error(("Entry '%s' in kind '%s' is deprecated, but missing deprecation description") \
288 % (fully_qualified_name(entry), find_kind(entry),
292 if entry.deprecation_description is not None:
293 validate_error(("Entry '%s' in kind '%s' has deprecation description, but is not deprecated") \
294 % (fully_qualified_name(entry), find_kind(entry),
300 def validate_xml(xml):
302 Validate all XML nodes according to the rules in validate_clones and
306 xml - A string containing a block of XML to validate
309 a BeautifulSoup instance if validation succeeds, None otherwise
312 soup = BeautifulSoup(xml, features='xml')
314 succ = validate_clones(soup)
315 succ = validate_entries(soup) and succ
322 #####################
323 #####################
325 if __name__ == "__main__":
326 if len(sys.argv) <= 1:
327 print >> sys.stderr, "Usage: %s <filename.xml>" % (sys.argv[0])
330 file_name = sys.argv[1]
331 succ = validate_xml(file(file_name).read()) is not None
334 print "%s: SUCCESS! Document validated" %(file_name)
337 print >> sys.stderr, "%s: ERRORS: Document failed to validate" %(file_name)