blob: c82b66cfb18eb9e06f99b5514d54d60699f3c7a8 [file] [log] [blame]
#
# 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