Home | History | Annotate | Download | only in docs
      1 #!/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 """
     20 Usage:
     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 
     26 Module:
     27   A set of helpful functions for dealing with BeautifulSoup element trees.
     28   Especially the find_* and fully_qualified_name functions.
     29 
     30 Dependencies:
     31   BeautifulSoup - an HTML/XML parser available to download from
     32                   http://www.crummy.com/software/BeautifulSoup/
     33 """
     34 
     35 from bs4 import BeautifulSoup
     36 from bs4 import Tag
     37 import sys
     38 
     39 
     40 #####################
     41 #####################
     42 
     43 def 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            or a <clone ...> XML node.
     51 
     52   Raises:
     53     ValueError: if entry does not correspond to one of the above XML nodes
     54 
     55   Returns:
     56     A string with the full name, e.g. "android.lens.info.availableApertureSizes"
     57   """
     58 
     59   filter_tags = ['namespace', 'section']
     60   parents = [i['name'] for i in entry.parents if i.name in filter_tags]
     61 
     62   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))
     69 
     70   parents.reverse()
     71   parents.append(name)
     72 
     73   fqn = ".".join(parents)
     74 
     75   return fqn
     76 
     77 def 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 
    105 def 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 
    133 def 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 
    161 def 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 
    179 def validate_error(msg):
    180   """
    181   Print a validation error to stderr.
    182 
    183   Args:
    184     msg: a string you want to be printed
    185   """
    186   print >> sys.stderr, "ERROR: " + msg
    187 
    188 
    189 def 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 
    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)
    223       success = False
    224 
    225   return success
    226 
    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):
    231   """
    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
    236 
    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 
    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),
    262                                   ))
    263         success = False
    264 
    265       else:
    266         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
    277     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
    283 
    284   return success
    285 
    286 def validate_xml(xml):
    287   """
    288   Validate all XML nodes according to the rules in validate_clones and
    289   validate_entries.
    290 
    291   Args:
    292     xml - A string containing a block of XML to validate
    293 
    294   Returns:
    295     a BeautifulSoup instance if validation succeeds, None otherwise
    296   """
    297 
    298   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 
    311 if __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]
    317   succ = validate_xml(file(file_name).read()) is not None
    318 
    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)
    325