| # |
| # Copyright (C) 2016 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Wraps nested XML file loading for specific elements.""" |
| |
| import os |
| |
| from project import common |
| from project import xml_parser |
| |
| |
| class PathElementLoader(object): |
| """Processes an XML element node with either a 'path' attribute |
| or child nodes. All attributes in the attrib_rollover list |
| will be propagated down to the final returned root node. |
| """ |
| def __init__(self, tag, rollover_attribs): |
| self._tag = tag |
| self._rollover_names = rollover_attribs |
| self._rollover_attribs = {} |
| # List of origins for any parsed @tag |
| # nodes. This is only a list because |
| # @tag may load a different @tag node. |
| self._origins = [] |
| |
| @property |
| def origins(self): |
| """Return the list of Origin objects parsed for this loader.""" |
| return self._origins |
| |
| def load(self, **source): |
| """Populates the instance from an XML file or element. |
| |
| Dereferences elements that may include other files returning |
| the final defined element. |
| |
| Args: |
| **source: May have one of the following keys set: |
| - 'path': The path (str) to a file to open, read, and parse. |
| - 'file': A file-compatible object to read from for parsing. |
| - 'element': Provides the root element node to walk. |
| |
| Returns: |
| Actual root node with overridden rollover attributes and |
| a new 'old_attribs' attribute containing the most recent root node's |
| original values. |
| |
| Raises |
| LoadError: LoadError, or a subclass, will be raised if an error |
| occurs loading and validating content from the XML. |
| """ |
| ALLOWED_KEYS = ('path', 'file', 'element') |
| if len(source) != 1 or source.keys()[0] not in ALLOWED_KEYS: |
| raise ValueError( |
| 'Exactly one of {} must be supplied: {}'.format(ALLOWED_KEYS, |
| source)) |
| if 'element' in source: |
| return self._load_element(source['element']) |
| |
| src = '' |
| if 'path' in source: |
| src = os.path.abspath(source['path']) |
| elif 'file' in source: |
| src = source['file'] |
| root = self._load_file(src) |
| if root is None: |
| raise common.LoadError( |
| 'Failed to access and read file: {}'.format(src)) |
| return root |
| |
| def _load_file(self, f): |
| seen = [x for x in self._origins if x.source_file == f] |
| if len(seen): |
| # Break cycles: A -> B -> A |
| print 'Warning: path "{}" included more than once.'.format(f) |
| return None |
| try: |
| tree = xml_parser.parse(f) |
| except IOError: |
| return None |
| root = tree.getroot() |
| if root.tag != self._tag: |
| # pylint: disable=no-member |
| raise common.LoadErrorWithOrigin( |
| root.origin, |
| 'Included file "{}" root node is not a <{}>'.format(f, |
| self._tag)) |
| return self._load_element(root) |
| |
| def _load_element(self, root): |
| self._origins += [root.origin.copy()] |
| # Overwrite any attribs from rollover_attribs. |
| root.old_attrib = {} |
| for attr in self._rollover_attribs: |
| if attr in root.attrib: |
| root.old_attrib[attr] = root.attrib[attr] |
| root.attrib[attr] = self._rollover_attribs[attr] |
| # Copy out any attribs that should rollover. |
| for attr in root.attrib: |
| if attr in self._rollover_names: |
| self._rollover_attribs[attr] = root.attrib[attr] |
| if 'path' in root.attrib: |
| path = root.get_attrib('path') |
| # Resolve relative paths using the including file's path. |
| if path[0] != '/': |
| caller = root.origin.source_file |
| path = os.path.join(os.path.dirname(caller), path) |
| # Populate internal state from the given path. |
| new_root = self._load_file(path) |
| if new_root is None: |
| raise common.LoadErrorWithOrigin( |
| root.origin, |
| 'Unable to load <{}> from file: {}'.format(self._tag, path)) |
| return new_root |
| return root |