# -*- coding: utf-8 -*-
#
# Copyright (c), 2015-2016, Quantum Espresso Foundation and SISSA (Scuola
# Internazionale Superiore di Studi Avanzati). All rights reserved.
# This file is distributed under the terms of the MIT License. See the
# file 'LICENSE' in the root directory of the present distribution, or
# http://opensource.org/licenses/MIT.
# Authors: Davide Brunato
#
"""
This module contains XMLSchema class for xsdtypes package
"""
import logging
from xml.etree import ElementTree
from .exceptions import FileFormatError
from .exceptions import XMLSchemaValidationError
from .xsdtypes import _ATTRIBUTE_TAG
from .xsdtypes import XSD_NAMESPACE_PATH
from .xsdtypes import XSI_ATTRIBUTES
from .xsdtypes import XSI_NAMESPACE_PATH
from .xsdtypes import add_xsd_types_to_dict
from .xsdtypes import xsd_attribute_type_factory
from .xsdtypes import xsd_complex_type_factory
from .xsdtypes import xsd_element_factory
from .xsdtypes import xsd_get_type
from .xsdtypes import xsd_simple_type_factory
logger = logging.getLogger('qespresso')
[docs]class XMLSchema(object):
"""
Class to wrap an XML Schema for types lookups.
"""
XML_SCHEMA_NAMESPACES = {
'xsd': XSD_NAMESPACE_PATH,
'xs': XSD_NAMESPACE_PATH,
'xsi': XSI_NAMESPACE_PATH
}
[docs] def __init__(self, xsd_file):
self._xsd = None
self._xml_schema = None
self.namespaces = self.XML_SCHEMA_NAMESPACES.copy()
"Namespaces used by XSD file"
self.target_namespace = None
"Target namespace URI"
self.target_prefix = ''
"Namespace prefix for declarations"
# Temporary mods for preserve types declaration order for Fortran generation
from collections import OrderedDict
self.types = OrderedDict() # {}
"Map XSD global types to XSDType instance"
self.attributes = {}
"Map XSD global attributes to XSDType instance"
self.attribute_groups = {}
"Group XSD attributes definitions"
self.elements = {}
"Map XSD global elements to XSDType instance"
self.groups = {}
"Group XSD elements definitions"
try:
self._xsd = ElementTree.parse(xsd_file)
except ElementTree.ParseError as e:
raise FileFormatError('XML', xsd_file, e.msg)
else:
self._xsd_file = xsd_file
try:
self._xml_schema = ElementTree.XMLSchema(self._xsd)
except AttributeError:
logger.info("XSD validation is not available!")
except ElementTree.XMLSchemaError as e:
logger.error("XSD schema error: %s " % repr(e))
raise
# Set the namespaces information
schema_elem = self.getroot()
self.target_namespace = schema_elem.attrib['targetNamespace']
logger.debug("Target namespace: %s" % self.target_namespace)
if hasattr(schema_elem, 'nsmap'):
# lxml loaded
for prefix, namespace in schema_elem.nsmap.items():
self.namespaces[prefix or
''] = namespace # Change None key with ''
else:
# xml.etree.ElementTree loaded: need to get namespaces from file
for event, node in ElementTree.iterparse(self._xsd_file,
events=['start-ns']):
self.namespaces[node[0]] = node[1]
logger.debug('Namespaces: {0}'.format(self.namespaces))
# Set namespace prefix for schema declarations
for prefix in self.namespaces:
if self.namespaces[prefix] == self.target_namespace:
self.target_prefix = prefix
if prefix:
prefix = '%s:' % prefix
break
else:
prefix = ''
logger.debug("Use prefix '%s' for schema declarations" % prefix)
# Build lookup maps using XSD declarations
logger.debug("### Add global simple types ###")
add_xsd_types_to_dict(self.findall('./xsd:simpleType'),
self.types,
xsd_simple_type_factory,
xsd_types=self.types,
prefix=prefix)
logger.debug("### Add global attributes ###")
counter = 0
for elem in self.findall('./xsd:attributes'):
self.attributes.update([
xsd_attribute_type_factory(elem,
xsd_types=self.types,
prefix=prefix)
])
counter += 1
logger.debug("%d global attributes added" % counter)
logger.debug("### Add attribute groups ###")
counter = 0
for elem in self.findall('./xsd:attributeGroup'):
try:
name = elem.attrib['name']
except KeyError:
raise XMLSchemaValidationError(
"Missing 'name' attribute for {}".format(elem))
if name in self.attribute_groups:
raise XMLSchemaValidationError(
"Duplicate attributeGroup: {}".format(name))
self.attribute_groups[name] = OrderedDict([
xsd_attribute_type_factory(child,
xsd_types=self.types,
prefix=prefix)
for child in elem
if child.tag == _ATTRIBUTE_TAG
])
counter += 1
logger.debug("%d attribute groups added" % counter)
logger.debug("### Add global complex types ###")
add_xsd_types_to_dict(self.findall('./xsd:complexType'),
self.types,
xsd_complex_type_factory,
xsd_types=self.types,
prefix=prefix,
xsd_attributes=self.attributes)
logger.debug("### Add content model groups ###")
counter = 0
for elem in self.findall('./xsd:group'):
try:
name = elem.attrib['name']
except KeyError:
raise XMLSchemaValidationError(
"Missing 'name' attribute for {}".format(elem))
if name in self.groups:
raise XMLSchemaValidationError(
"Duplicate group: {}".format(name))
from .xsdtypes import xsd_group_factory
self.groups[name] = xsd_group_factory(
elem,
xsd_groups=self.groups,
xsd_types=self.types,
xsd_attributes=self.attributes,
xsd_attribute_groups=self.attribute_groups)
counter += 1
logger.debug("%d XSD named groups added" % counter)
logger.debug("### Add elements starting from root element(s) ###")
for elem in self.findall('./xsd:element'):
xsd_element_factory(elem,
parent_path=None,
prefix=prefix,
xsd_elements=self.elements,
xsd_types=self.types,
xsd_attributes=self.attributes,
xsd_attribute_groups=self.attribute_groups)
logger.debug("%d XSD elements added" % len(self.elements))
[docs] def get_type(self, type_name):
"""
Return the XSD type instance corresponding to the argument.
:param type_name: Name of the type. Types in the schema's
namespace have to be provided with namespace URI or prefix.
Unqualified names are interpreted as typed of xsd/xs namespace.
:return: XSDType or XSDSimpleType instance
"""
logger.debug("XSD type name: '{0}'".format(type_name))
return xsd_get_type(type_name, self.types)
[docs] def get_element(self, element_path):
element_path = element_path.replace('/{%s}' % self.target_namespace, '/')\
.replace('/%s:' % self.target_prefix, '/')
try:
return self.elements[element_path]
except KeyError:
raise ValueError("Element '%s' not found in XSD schema!" %
element_path)
[docs] def get_element_tag(self, element_path):
"""
Return the XSD type instance of the element.
:param element_path: The absolute path to the element. Schema's
namespace prefixes or URIs are stripped from the path.
:return: XSDType, XSDSimpleType or XSDComplexType instance
"""
try:
name = self.get_element(element_path).name
if self.target_prefix and name.startswith(
'%s:' % self.target_prefix):
return name.replace('%s:' % self.target_prefix, '')
except KeyError:
raise ValueError("Element '%s' not found in XSD schema!" %
element_path)
[docs] def get_attributes(self, element_path):
element = self.get_element(element_path)
try:
return element.xsd_type.attributes
except AttributeError:
return dict()
[docs] def get_element_type(self, element_path):
"""
Return the XSD type instance of the element.
:param element_path: The absolute path to the element. Schema's
namespace prefixes or URIs are stripped from the path.
:return: XSDType, XSDSimpleType or XSDComplexType instance
"""
try:
return self.get_element(element_path).get_type()
except KeyError:
raise ValueError("Element '%s' not found in XSD schema!" %
element_path)
[docs] def get_attribute_type(self, attribute_name, element_path):
"""
Return the XSD type instance of the attribute.
:param attribute_name: The name of the attribute. Schema's
namespace prefix or URI is stripped from the name.
:param element_path: The absolute path to the element. Schema's
namespace prefixes or URIs are stripped from the path.
:return: XSDType or XSDSimpleType instance
"""
attribute_name = attribute_name.replace('{%s}' % self.target_namespace, '/')\
.replace('%s:' % self.target_prefix, '/')
try:
xsd_type = self.get_element(
element_path).xsd_type.attributes[attribute_name].xsd_type
except KeyError:
try:
return XSI_ATTRIBUTES[attribute_name].xsd_type
except KeyError:
raise ValueError("Attribute '%s' not found in XSD schema!" %
attribute_name)
else:
logger.debug("Map attribute '%s' to type '%s'" %
(attribute_name, xsd_type.name))
return xsd_type
[docs] def get_element_default(self, element_path):
"""
Return the default of the element.
:param element_path: The absolute path to the element. Schema's
namespace prefixes or URIs are stripped from the path.
:return: XSDType, XSDSimpleType or XSDComplexType instance
"""
return self.get_element(element_path).get_default()
[docs] def get_attribute_default(self, attribute_name, element_path):
"""
Return the XSD type instance of the attribute.
:param attribute_name: The name of the attribute. Schema's
namespace prefix or URI is stripped from the name.
:param element_path: The absolute path to the element. Schema's
namespace prefixes or URIs are stripped from the path.
:return: XSDType or XSDSimpleType instance
"""
try:
xsd_attribute = self.get_attributes(element_path)[attribute_name]
except KeyError:
msg = "Attribute '%s' not found in XSD element '%s'!"
raise ValueError(msg % (attribute_name, element_path))
else:
return xsd_attribute.get_default()
[docs] def find(self, path, namespaces=None):
"""
Find first matching element by tag name or path.
:param path: is a string having either an element tag or an XPath,
:param namespaces: is an optional mapping from namespace prefix to full name.
:return: the first matching element, or None if no element was found
"""
namespaces = namespaces or {}
namespaces.update(self.namespaces)
return self._xsd.find(path, namespaces)
[docs] def findall(self, path, namespaces=None):
"""
Find all matching subelements by tag name or path.
:param path: is a string having either an element tag or an XPath,
:param namespaces: is an optional mapping from namespace prefix to full name.
:return: the first matching element, or None if no element was found
"""
namespaces = namespaces or {}
namespaces.update(self.namespaces)
return self._xsd.findall(path, namespaces)
[docs] def getroot(self):
"""Return root element of the XML schema tree."""
return self._xsd.getroot()
[docs] def iselement(self, elem):
"""
Checks if an element instance appears to be a valid
element object.
:param elem:
"""
return self._xsd.iselement(elem)
[docs] def iter(self, tag=None):
"""
Create and return an iterator that loops over all elements
in this tree, in document order.
:param tag: is a string with the tag name to iterate over
(default is to return all elements).
"""
if tag is not None and tag[0] != '{':
tag = '{%s}%s' % (XSD_NAMESPACE_PATH, tag)
return self._xsd.iter(tag)
[docs] def validate(self, xml_config):
"""
Validate the configuration with XSD and with optional parameter dependencies.
:param xml_config:
"""
try:
import lxml.etree as ET
except ImportError:
logger.debug("")
return
xsd = ET.parse(self._xsd_file)
xml_schema = ET.XMLSchema(xsd)
try:
xml_schema.assertValid(xml_config.tostring())
except AttributeError:
logger.warning("XML Schema validation not available!")
except ET.DocumentInvalid as e:
raise XMLSchemaValidationError(e)