blob: 0dc4ae986c88a9a840f58ec19eb19db7a18c197d [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:
49 entry: a BeautifulSoup Tag corresponding to an <entry ...> XML node
50
51 Returns:
52 A string with the full name, e.g. "android.lens.info.availableApertureSizes"
53 """
54 filter_tags = ['namespace', 'section']
55 parents = [i['name'] for i in entry.parents if i.name in filter_tags]
56
57 name = entry['name']
58
59 parents.reverse()
60 parents.append(name)
61
62 fqn = ".".join(parents)
63
64 return fqn
65
66def find_parent_by_name(element, names):
67 """
68 Find the ancestor for an element whose name matches one of those
69 in names.
70
71 Args:
72 element: A BeautifulSoup Tag corresponding to an XML node
73
74 Returns:
75 A BeautifulSoup element corresponding to the matched parent, or None.
76
77 For example, assuming the following XML structure:
78 <static>
79 <anything>
80 <entry name="Hello" /> # this is in variable 'Hello'
81 </anything>
82 </static>
83
84 el = find_parent_by_name(Hello, ['static'])
85 # el is now a value pointing to the '<static>' element
86 """
87 matching_parents = [i.name for i in element.parents if i.name in names]
88
89 if matching_parents:
90 return matching_parents[0]
91 else:
92 return None
93
Igor Murashkin96bd0192012-11-19 16:49:37 -080094def find_all_child_tags(element, tag):
95 """
96 Finds all the children that are a Tag (as opposed to a NavigableString),
97 with a name of tag. This is useful to filter out the NavigableString out
98 of the children.
99
100 Args:
101 element: A BeautifulSoup Tag corresponding to an XML node
102 tag: A string representing the name of the tag
103
104 Returns:
105 A list of Tag instances
106
107 For example, given the following XML structure:
108 <enum> # This is the variable el
109 Hello world # NavigableString
110 <value>Apple</value> # this is the variale apple (Tag)
111 <value>Orange</value> # this is the variable orange (Tag)
112 Hello world again # NavigableString
113 </enum>
114
115 lst = find_all_child_tags(el, 'value')
116 # lst is [apple, orange]
117
118 """
119 matching_tags = [i for i in element.children if isinstance(i, Tag) and i.name == tag]
120 return matching_tags
121
122def find_child_tag(element, tag):
123 """
124 Finds the first child that is a Tag with the matching name.
125
126 Args:
127 element: a BeautifulSoup Tag
128 tag: A String representing the name of the tag
129
130 Returns:
131 An instance of a Tag, or None if there was no matches.
132
133 For example, given the following XML structure:
134 <enum> # This is the variable el
135 Hello world # NavigableString
136 <value>Apple</value> # this is the variale apple (Tag)
137 <value>Orange</value> # this is the variable orange (Tag)
138 Hello world again # NavigableString
139 </enum>
140
141 res = find_child_tag(el, 'value')
142 # res is apple
143 """
144 matching_tags = find_all_child_tags(element, tag)
145 if matching_tags:
146 return matching_tags[0]
147 else:
148 return None
149
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800150def find_kind(element):
151 """
152 Finds the kind Tag ancestor for an element.
153
154 Args:
155 element: a BeautifulSoup Tag
156
157 Returns:
158 a BeautifulSoup tag, or None if there was no matches
159
160 Remarks:
161 This function only makes sense to be called for an Entry, Clone, or
162 InnerNamespace XML types. It will always return 'None' for other nodes.
163 """
164 kinds = ['dynamic', 'static', 'controls']
165 parent_kind = find_parent_by_name(element, kinds)
166 return parent_kind
167
168def validate_error(msg):
169 """
170 Print a validation error to stderr.
171
172 Args:
173 msg: a string you want to be printed
174 """
175 print >> sys.stderr, "Validation error: " + msg
176
177
178def validate_clones(soup):
179 """
180 Validate that all <clone> elements point to an existing <entry> element.
181
182 Args:
183 soup - an instance of BeautifulSoup
184
185 Returns:
186 True if the validation succeeds, False otherwise
187 """
188 success = True
189
190 for clone in soup.find_all("clone"):
191 clone_entry = clone['entry']
192 clone_kind = clone['kind']
193
194 parent_kind = find_kind(clone)
195
196 find_entry = lambda x: x.name == 'entry' \
197 and find_kind(x) == clone_kind \
198 and fully_qualified_name(x) == clone_entry
199 matching_entry = soup.find(find_entry)
200
201 if matching_entry is None:
202 error_msg = ("Did not find corresponding clone entry '%s' " + \
203 "with kind '%s'") %(clone_entry, clone_kind)
204 validate_error(error_msg)
205 success = False
206
207 return success
208
209# All <entry> elements with container=$foo have a <$foo> child
Igor Murashkin96bd0192012-11-19 16:49:37 -0800210# If type="enum", <enum> tag is present
211# In <enum> for all <value id="$x">, $x is numeric
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800212def validate_entries(soup):
213 """
214 Validate all <entry> elements with the following rules:
215 * If there is a container="$foo" attribute, there is a <$foo> child
Igor Murashkin96bd0192012-11-19 16:49:37 -0800216 * If there is a type="enum" attribute, there is an <enum> child
217 * In the <enum> child, all <value id="$x"> have a numeric $x
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800218
219 Args:
220 soup - an instance of BeautifulSoup
221
222 Returns:
223 True if the validation succeeds, False otherwise
224 """
225 success = True
226 for entry in soup.find_all("entry"):
227 entry_container = entry.attrs.get('container')
228
229 if entry_container is not None:
230 container_tag = entry.find(entry_container)
231
232 if container_tag is None:
233 success = False
234 validate_error(("Entry '%s' in kind '%s' has type '%s' but " + \
235 "missing child element <%s>") \
236 %(fully_qualified_name(entry), find_kind(entry), \
237 entry_container, entry_container))
238
Igor Murashkinb556bc42012-12-04 16:07:21 -0800239 enum = entry.attrs.get('enum')
240 if enum and enum == 'true':
Igor Murashkin96bd0192012-11-19 16:49:37 -0800241 if entry.enum is None:
242 validate_error(("Entry '%s' in kind '%s' is missing enum") \
243 % (fully_qualified_name(entry), find_kind(entry),
244 ))
Igor Murashkinb556bc42012-12-04 16:07:21 -0800245 success = False
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800246
Igor Murashkinb556bc42012-12-04 16:07:21 -0800247 else:
Igor Murashkin96bd0192012-11-19 16:49:37 -0800248 for value in entry.enum.find_all('value'):
249 value_id = value.attrs.get('id')
250
251 if value_id is not None:
252 try:
253 id_int = int(value_id, 0) #autoguess base
254 except ValueError:
255 validate_error(("Entry '%s' has id '%s', which is not" + \
256 " numeric.") \
257 %(fully_qualified_name(entry), value_id))
258 success = False
Igor Murashkinb556bc42012-12-04 16:07:21 -0800259 else:
260 if entry.enum:
261 validate_error(("Entry '%s' kind '%s' has enum el, but no enum attr") \
262 % (fully_qualified_name(entry), find_kind(entry),
263 ))
264 success = False
Igor Murashkin96bd0192012-11-19 16:49:37 -0800265
266 return success
Igor Murashkin77b63ca2012-11-09 16:15:02 -0800267
268def validate_xml(file_name):
269 """
270 Validate all XML nodes according to the rules in validate_clones and
271 validate_entries.
272
273 Args:
274 file_name - a string path to an XML file we wish to validate
275
276 Returns:
277 a BeautifulSoup instance if validation succeeds, None otherwise
278 """
279
280 xml = file(file_name).read()
281 soup = BeautifulSoup(xml, features='xml')
282
283 succ = validate_clones(soup)
284 succ = validate_entries(soup) and succ
285
286 if succ:
287 return soup
288 else:
289 return None
290
291#####################
292#####################
293
294if __name__ == "__main__":
295 if len(sys.argv) <= 1:
296 print >> sys.stderr, "Usage: %s <filename.xml>" % (sys.argv[0])
297 sys.exit(0)
298
299 file_name = sys.argv[1]
300 succ = validate_xml(file_name) is not None
301
302 if succ:
303 print "%s: SUCCESS! Document validated" %(file_name)
304 sys.exit(0)
305 else:
306 print >> sys.stderr, "%s: ERRORS: Document failed to validate" %(file_name)
307 sys.exit(1)