blob: 8dc6050885c50f0800dc0732f8d355618d95aeb9 [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
36import sys
37
38
39#####################
40#####################
41
42def fully_qualified_name(entry):
43 """
44 Calculates the fully qualified name for an entry by walking the path
45 to the root node.
46
47 Args:
48 entry: a BeautifulSoup Tag corresponding to an <entry ...> XML node
49
50 Returns:
51 A string with the full name, e.g. "android.lens.info.availableApertureSizes"
52 """
53 filter_tags = ['namespace', 'section']
54 parents = [i['name'] for i in entry.parents if i.name in filter_tags]
55
56 name = entry['name']
57
58 parents.reverse()
59 parents.append(name)
60
61 fqn = ".".join(parents)
62
63 return fqn
64
65def find_parent_by_name(element, names):
66 """
67 Find the ancestor for an element whose name matches one of those
68 in names.
69
70 Args:
71 element: A BeautifulSoup Tag corresponding to an XML node
72
73 Returns:
74 A BeautifulSoup element corresponding to the matched parent, or None.
75
76 For example, assuming the following XML structure:
77 <static>
78 <anything>
79 <entry name="Hello" /> # this is in variable 'Hello'
80 </anything>
81 </static>
82
83 el = find_parent_by_name(Hello, ['static'])
84 # el is now a value pointing to the '<static>' element
85 """
86 matching_parents = [i.name for i in element.parents if i.name in names]
87
88 if matching_parents:
89 return matching_parents[0]
90 else:
91 return None
92
93def find_kind(element):
94 """
95 Finds the kind Tag ancestor for an element.
96
97 Args:
98 element: a BeautifulSoup Tag
99
100 Returns:
101 a BeautifulSoup tag, or None if there was no matches
102
103 Remarks:
104 This function only makes sense to be called for an Entry, Clone, or
105 InnerNamespace XML types. It will always return 'None' for other nodes.
106 """
107 kinds = ['dynamic', 'static', 'controls']
108 parent_kind = find_parent_by_name(element, kinds)
109 return parent_kind
110
111def validate_error(msg):
112 """
113 Print a validation error to stderr.
114
115 Args:
116 msg: a string you want to be printed
117 """
118 print >> sys.stderr, "Validation error: " + msg
119
120
121def validate_clones(soup):
122 """
123 Validate that all <clone> elements point to an existing <entry> element.
124
125 Args:
126 soup - an instance of BeautifulSoup
127
128 Returns:
129 True if the validation succeeds, False otherwise
130 """
131 success = True
132
133 for clone in soup.find_all("clone"):
134 clone_entry = clone['entry']
135 clone_kind = clone['kind']
136
137 parent_kind = find_kind(clone)
138
139 find_entry = lambda x: x.name == 'entry' \
140 and find_kind(x) == clone_kind \
141 and fully_qualified_name(x) == clone_entry
142 matching_entry = soup.find(find_entry)
143
144 if matching_entry is None:
145 error_msg = ("Did not find corresponding clone entry '%s' " + \
146 "with kind '%s'") %(clone_entry, clone_kind)
147 validate_error(error_msg)
148 success = False
149
150 return success
151
152# All <entry> elements with container=$foo have a <$foo> child
153def validate_entries(soup):
154 """
155 Validate all <entry> elements with the following rules:
156 * If there is a container="$foo" attribute, there is a <$foo> child
157
158 Args:
159 soup - an instance of BeautifulSoup
160
161 Returns:
162 True if the validation succeeds, False otherwise
163 """
164 success = True
165 for entry in soup.find_all("entry"):
166 entry_container = entry.attrs.get('container')
167
168 if entry_container is not None:
169 container_tag = entry.find(entry_container)
170
171 if container_tag is None:
172 success = False
173 validate_error(("Entry '%s' in kind '%s' has type '%s' but " + \
174 "missing child element <%s>") \
175 %(fully_qualified_name(entry), find_kind(entry), \
176 entry_container, entry_container))
177
178 return success
179
180
181def validate_xml(file_name):
182 """
183 Validate all XML nodes according to the rules in validate_clones and
184 validate_entries.
185
186 Args:
187 file_name - a string path to an XML file we wish to validate
188
189 Returns:
190 a BeautifulSoup instance if validation succeeds, None otherwise
191 """
192
193 xml = file(file_name).read()
194 soup = BeautifulSoup(xml, features='xml')
195
196 succ = validate_clones(soup)
197 succ = validate_entries(soup) and succ
198
199 if succ:
200 return soup
201 else:
202 return None
203
204#####################
205#####################
206
207if __name__ == "__main__":
208 if len(sys.argv) <= 1:
209 print >> sys.stderr, "Usage: %s <filename.xml>" % (sys.argv[0])
210 sys.exit(0)
211
212 file_name = sys.argv[1]
213 succ = validate_xml(file_name) is not None
214
215 if succ:
216 print "%s: SUCCESS! Document validated" %(file_name)
217 sys.exit(0)
218 else:
219 print >> sys.stderr, "%s: ERRORS: Document failed to validate" %(file_name)
220 sys.exit(1)