blob: 826000592ec78cb1aec44da95db7b8fb32a67181 [file] [log] [blame]
Igor Murashkin77b63ca2012-11-09 16:15:02 -08001#!/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"""
20Usage:
21 metadata_validate.py <filename.xml>
22 - validates that the metadata properties defined in filename.xml are
23 semantically correct.
24 - does not do any XSD validation, use xmllint for that (in metadata-validate)
25
26Module:
27 A set of helpful functions for dealing with BeautifulSoup element trees.
28 Especially the find_* and fully_qualified_name functions.
29
30Dependencies:
31 BeautifulSoup - an HTML/XML parser available to download from
32 http://www.crummy.com/software/BeautifulSoup/
33"""
34
35from bs4 import BeautifulSoup
Igor Murashkin96bd0192012-11-19 16:49:37 -080036from bs4 import Tag
Igor Murashkin77b63ca2012-11-09 16:15:02 -080037import sys
38
39
40#####################
41#####################
42
43def fully_qualified_name(entry):
44 """
45 Calculates the fully qualified name for an entry by walking the path
46 to the root node.
47
48 Args:
Igor Murashkin23fed4b2014-01-09 17:44:57 -080049 entry: a BeautifulSoup Tag corresponding to an <entry ...> XML node,
50 or a <clone ...> XML node.
51
52 Raises:
53 ValueError: if entry does not correspond to one of the above XML nodes
Igor Murashkin77b63ca2012-11-09 16:15:02 -080054
55 Returns:
56 A string with the full name, e.g. "android.lens.info.availableApertureSizes"
57 """
Igor Murashkin23fed4b2014-01-09 17:44:57 -080058
Igor Murashkin77b63ca2012-11-09 16:15:02 -080059 filter_tags = ['namespace', 'section']
60 parents = [i['name'] for i in entry.parents if i.name in filter_tags]
61
Igor Murashkin23fed4b2014-01-09 17:44:57 -080062 if entry.name == 'entry':
63 name = entry['name']
64 elif entry.name == 'clone':
65 name = entry['entry'].split(".")[-1] # "a.b.c" => "c"
66 else:
67 raise ValueError("Unsupported tag type '%s' for element '%s'" \
68 %(entry.name, entry))
Igor Murashkin77b63ca2012-11-09 16:15:02 -080069
70 parents.reverse()
71 parents.append(name)
72
73 fqn = ".".join(parents)
74
75 return fqn
76
77def find_parent_by_name(element, names):
78 """
79 Find the ancestor for an element whose name matches one of those
80 in names.
81
82 Args:
83 element: A BeautifulSoup Tag corresponding to an XML node
84
85 Returns:
86 A BeautifulSoup element corresponding to the matched parent, or None.
87
88 For example, assuming the following XML structure:
89 <static>
90 <anything>
91 <entry name="Hello" /> # this is in variable 'Hello'
92 </anything>
93 </static>
94
95 el = find_parent_by_name(Hello, ['static'])
96 # el is now a value pointing to the '<static>' element
97 """
98 matching_parents = [i.name for i in element.parents if i.name in names]
99
100 if matching_parents:
101 return matching_parents[0]
102 else:
103 return None
104
Igor Murashkin96bd0192012-11-19 16:49:37 -0800105def find_all_child_tags(element, tag):
106 """
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
109 of the children.
110
111 Args:
112 element: A BeautifulSoup Tag corresponding to an XML node
113 tag: A string representing the name of the tag
114
115 Returns:
116 A list of Tag instances
117
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
124 </enum>
125
126 lst = find_all_child_tags(el, 'value')
127 # lst is [apple, orange]
128
129 """
130 matching_tags = [i for i in element.children if isinstance(i, Tag) and i.name == tag]
131 return matching_tags
132
133def find_child_tag(element, tag):
134 """
135 Finds the first child that is a Tag with the matching name.
136
137 Args:
138 element: a BeautifulSoup Tag
139 tag: A String representing the name of the tag
140
141 Returns:
142 An instance of a Tag, or None if there was no matches.
143
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
150 </enum>
151
152 res = find_child_tag(el, 'value')
153 # res is apple
154 """
155 matching_tags = find_all_child_tags(element, tag)
156 if matching_tags:
157 return matching_tags[0]
158 else:
159 return None
160
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800161def find_kind(element):
162 """
163 Finds the kind Tag ancestor for an element.
164
165 Args:
166 element: a BeautifulSoup Tag
167
168 Returns:
169 a BeautifulSoup tag, or None if there was no matches
170
171 Remarks:
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.
174 """
175 kinds = ['dynamic', 'static', 'controls']
176 parent_kind = find_parent_by_name(element, kinds)
177 return parent_kind
178
179def validate_error(msg):
180 """
181 Print a validation error to stderr.
182
183 Args:
184 msg: a string you want to be printed
185 """
Igor Murashkin23fed4b2014-01-09 17:44:57 -0800186 print >> sys.stderr, "ERROR: " + msg
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800187
188
189def validate_clones(soup):
190 """
191 Validate that all <clone> elements point to an existing <entry> element.
192
193 Args:
194 soup - an instance of BeautifulSoup
195
196 Returns:
197 True if the validation succeeds, False otherwise
198 """
199 success = True
200
201 for clone in soup.find_all("clone"):
202 clone_entry = clone['entry']
203 clone_kind = clone['kind']
204
205 parent_kind = find_kind(clone)
206
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)
211
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)
216 success = False
217
Igor Murashkin23fed4b2014-01-09 17:44:57 -0800218 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)
223 success = False
224
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800225 return success
226
227# All <entry> elements with container=$foo have a <$foo> child
Igor Murashkin96bd0192012-11-19 16:49:37 -0800228# If type="enum", <enum> tag is present
229# In <enum> for all <value id="$x">, $x is numeric
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800230def validate_entries(soup):
231 """
232 Validate all <entry> elements with the following rules:
233 * If there is a container="$foo" attribute, there is a <$foo> child
Igor Murashkin96bd0192012-11-19 16:49:37 -0800234 * 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
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800236
237 Args:
238 soup - an instance of BeautifulSoup
239
240 Returns:
241 True if the validation succeeds, False otherwise
242 """
243 success = True
244 for entry in soup.find_all("entry"):
245 entry_container = entry.attrs.get('container')
246
247 if entry_container is not None:
248 container_tag = entry.find(entry_container)
249
250 if container_tag is None:
251 success = False
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))
256
Igor Murashkinb556bc42012-12-04 16:07:21 -0800257 enum = entry.attrs.get('enum')
258 if enum and enum == 'true':
Igor Murashkin96bd0192012-11-19 16:49:37 -0800259 if entry.enum is None:
260 validate_error(("Entry '%s' in kind '%s' is missing enum") \
261 % (fully_qualified_name(entry), find_kind(entry),
262 ))
Igor Murashkinb556bc42012-12-04 16:07:21 -0800263 success = False
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800264
Igor Murashkinb556bc42012-12-04 16:07:21 -0800265 else:
Igor Murashkin96bd0192012-11-19 16:49:37 -0800266 for value in entry.enum.find_all('value'):
267 value_id = value.attrs.get('id')
268
269 if value_id is not None:
270 try:
271 id_int = int(value_id, 0) #autoguess base
272 except ValueError:
273 validate_error(("Entry '%s' has id '%s', which is not" + \
274 " numeric.") \
275 %(fully_qualified_name(entry), value_id))
276 success = False
Igor Murashkinb556bc42012-12-04 16:07:21 -0800277 else:
278 if entry.enum:
279 validate_error(("Entry '%s' kind '%s' has enum el, but no enum attr") \
280 % (fully_qualified_name(entry), find_kind(entry),
281 ))
282 success = False
Igor Murashkin96bd0192012-11-19 16:49:37 -0800283
284 return success
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800285
Eino-Ville Talvala63c0fb22014-01-02 16:11:44 -0800286def validate_xml(xml):
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800287 """
288 Validate all XML nodes according to the rules in validate_clones and
289 validate_entries.
290
291 Args:
Eino-Ville Talvala63c0fb22014-01-02 16:11:44 -0800292 xml - A string containing a block of XML to validate
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800293
294 Returns:
295 a BeautifulSoup instance if validation succeeds, None otherwise
296 """
297
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800298 soup = BeautifulSoup(xml, features='xml')
299
300 succ = validate_clones(soup)
301 succ = validate_entries(soup) and succ
302
303 if succ:
304 return soup
305 else:
306 return None
307
308#####################
309#####################
310
311if __name__ == "__main__":
312 if len(sys.argv) <= 1:
313 print >> sys.stderr, "Usage: %s <filename.xml>" % (sys.argv[0])
314 sys.exit(0)
315
316 file_name = sys.argv[1]
Igor Murashkin23fed4b2014-01-09 17:44:57 -0800317 succ = validate_xml(file(file_name).read()) is not None
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800318
319 if succ:
320 print "%s: SUCCESS! Document validated" %(file_name)
321 sys.exit(0)
322 else:
323 print >> sys.stderr, "%s: ERRORS: Document failed to validate" %(file_name)
324 sys.exit(1)