# -*- coding: utf-8 -*- # # Copyright (c), 2016-2019, SISSA (International School for Advanced Studies). # 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. # # @author Davide Brunato # """ This module contains functions and classes for namespaces XSD declarations/definitions. """ from __future__ import unicode_literals import warnings from collections import Counter from ..compat import string_base_type from ..exceptions import XMLSchemaKeyError, XMLSchemaTypeError, XMLSchemaValueError, XMLSchemaWarning from ..namespaces import XSD_NAMESPACE, NamespaceResourcesMap from ..qnames import XSD_REDEFINE, XSD_OVERRIDE, XSD_NOTATION, XSD_ANY_TYPE, \ XSD_SIMPLE_TYPE, XSD_COMPLEX_TYPE, XSD_GROUP, XSD_ATTRIBUTE, XSD_ATTRIBUTE_GROUP, \ XSD_ELEMENT, XSI_TYPE, get_qname, local_name, qname_to_extended from . import XMLSchemaNotBuiltError, XMLSchemaModelError, XMLSchemaModelDepthError, \ XsdValidator, XsdComponent, XsdAttribute, XsdSimpleType, XsdComplexType, XsdElement, \ XsdAttributeGroup, XsdGroup, XsdNotation, Xsd11Element, XsdKeyref, XsdAssert from .builtins import xsd_builtin_types_factory # # Defines the load functions for XML Schema structures def create_load_function(tag): def load_xsd_globals(xsd_globals, schemas): redefinitions = [] for schema in schemas: target_namespace = schema.target_namespace for elem in filter(lambda x: x.tag in (XSD_REDEFINE, XSD_OVERRIDE), schema.root): location = elem.get('schemaLocation') if location is None: continue for child in filter(lambda x: x.tag == tag and 'name' in x.attrib, elem): qname = get_qname(target_namespace, child.attrib['name']) redefinitions.append((qname, elem, child, schema, schema.includes[location])) for elem in filter(lambda x: x.tag == tag and 'name' in x.attrib, schema.root): qname = get_qname(target_namespace, elem.attrib['name']) if qname not in xsd_globals: xsd_globals[qname] = (elem, schema) else: try: other_schema = xsd_globals[qname][1] except (TypeError, IndexError): pass else: # It's ignored or replaced in case of an override if other_schema.override is schema: continue elif schema.override is other_schema: xsd_globals[qname] = (elem, schema) continue msg = "global {} with name={!r} is already defined" schema.parse_error(msg.format(local_name(tag), qname)) tags = Counter([x[0] for x in redefinitions]) for qname, elem, child, schema, redefined_schema in redefinitions: # Checks multiple redefinitions if tags[qname] > 1: tags[qname] = 1 redefined_schemas = [x[-1] for x in redefinitions if x[0] == qname] if any(redefined_schemas.count(x) > 1 for x in redefined_schemas): msg = "multiple redefinition for {} {!r}" schema.parse_error(msg.format(local_name(child.tag), qname), child) else: redefined_schemas = {x[-1]: x[-2] for x in redefinitions if x[0] == qname} for rs, s in redefined_schemas.items(): while True: try: s = redefined_schemas[s] except KeyError: break if s is rs: msg = "circular redefinition for {} {!r}" schema.parse_error(msg.format(local_name(child.tag), qname), child) break if elem.tag == XSD_OVERRIDE: xsd_globals[qname] = (child, schema) else: # Append to a list if it's a redefine try: xsd_globals[qname].append((child, schema)) except KeyError: schema.parse_error("not a redefinition!", child) except AttributeError: xsd_globals[qname] = [xsd_globals[qname], (child, schema)] return load_xsd_globals load_xsd_simple_types = create_load_function(XSD_SIMPLE_TYPE) load_xsd_attributes = create_load_function(XSD_ATTRIBUTE) load_xsd_attribute_groups = create_load_function(XSD_ATTRIBUTE_GROUP) load_xsd_complex_types = create_load_function(XSD_COMPLEX_TYPE) load_xsd_elements = create_load_function(XSD_ELEMENT) load_xsd_groups = create_load_function(XSD_GROUP) load_xsd_notations = create_load_function(XSD_NOTATION) def create_lookup_function(xsd_classes): if isinstance(xsd_classes, tuple): types_desc = ' or '.join([c.__name__ for c in xsd_classes]) else: types_desc = xsd_classes.__name__ def lookup(qname, global_map, tag_map): try: obj = global_map[qname] except KeyError: if '{' in qname: raise XMLSchemaKeyError("missing an %s component for %r!" % (types_desc, qname)) raise XMLSchemaKeyError("missing an %s component for %r! As the name has no namespace " "maybe a missing default namespace declaration." % (types_desc, qname)) else: if isinstance(obj, xsd_classes): return obj elif isinstance(obj, tuple): # Not built XSD global component without redefinitions try: elem, schema = obj except ValueError: return obj[0] # Circular build, simply return (elem, schema) couple try: factory_or_class = tag_map[elem.tag] except KeyError: raise XMLSchemaKeyError("wrong element %r for map %r." % (elem, global_map)) global_map[qname] = obj, # Encapsulate into a single-item tuple to catch circular builds global_map[qname] = factory_or_class(elem, schema, parent=None) return global_map[qname] elif isinstance(obj, list): # Not built XSD global component with redefinitions try: elem, schema = obj[0] except ValueError: return obj[0][0] # Circular build, simply return (elem, schema) couple try: factory_or_class = tag_map[elem.tag] except KeyError: raise XMLSchemaKeyError("wrong element %r for map %r." % (elem, global_map)) global_map[qname] = obj[0], # To catch circular builds global_map[qname] = component = factory_or_class(elem, schema, parent=None) # Apply redefinitions (changing elem involve a re-parsing of the component) for elem, schema in obj[1:]: component.redefine = component.copy() component.redefine.parent = component component.schema = schema component.elem = elem return global_map[qname] else: raise XMLSchemaTypeError( "wrong instance %s for XSD global %r, a %s required." % (obj, qname, types_desc) ) return lookup lookup_notation = create_lookup_function(XsdNotation) lookup_type = create_lookup_function((XsdSimpleType, XsdComplexType)) lookup_attribute = create_lookup_function(XsdAttribute) lookup_attribute_group = create_lookup_function(XsdAttributeGroup) lookup_group = create_lookup_function(XsdGroup) lookup_element = create_lookup_function(XsdElement) class XsdGlobals(XsdValidator): """ Mediator class for related XML schema instances. It stores the global declarations defined in the registered schemas. Register a schema to add it's declarations to the global maps. :param validator: the origin schema class/instance used for creating the global maps. :param validation: the XSD validation mode to use, can be 'strict', 'lax' or 'skip'. """ def __init__(self, validator, validation='strict'): super(XsdGlobals, self).__init__(validation) if not all(hasattr(validator, a) for a in ('meta_schema', 'BUILDERS_MAP')): raise XMLSchemaValueError("The argument {!r} is not an XSD schema validator".format(validator)) self.validator = validator self.namespaces = NamespaceResourcesMap() # Registered schemas by namespace URI self.types = {} # Global types (both complex and simple) self.attributes = {} # Global attributes self.attribute_groups = {} # Attribute groups self.groups = {} # Model groups self.notations = {} # Notations self.elements = {} # Global elements self.substitution_groups = {} # Substitution groups self.identities = {} # Identity constraints (uniqueness, keys, keyref) self.global_maps = (self.notations, self.types, self.attributes, self.attribute_groups, self.groups, self.elements) def __repr__(self): return '%s(validator=%r, validation=%r)' % (self.__class__.__name__, self.validator, self.validation) def copy(self, validator=None, validation=None): """Makes a copy of the object.""" obj = XsdGlobals(self.validator if validator is None else validator, validation or self.validation) obj.namespaces.update(self.namespaces) obj.types.update(self.types) obj.attributes.update(self.attributes) obj.attribute_groups.update(self.attribute_groups) obj.groups.update(self.groups) obj.notations.update(self.notations) obj.elements.update(self.elements) obj.substitution_groups.update(self.substitution_groups) obj.identities.update(self.identities) return obj __copy__ = copy def lookup_notation(self, qname): return lookup_notation(qname, self.notations, self.validator.BUILDERS_MAP) def lookup_type(self, qname): return lookup_type(qname, self.types, self.validator.BUILDERS_MAP) def lookup_attribute(self, qname): return lookup_attribute(qname, self.attributes, self.validator.BUILDERS_MAP) def lookup_attribute_group(self, qname): return lookup_attribute_group(qname, self.attribute_groups, self.validator.BUILDERS_MAP) def lookup_group(self, qname): return lookup_group(qname, self.groups, self.validator.BUILDERS_MAP) def lookup_element(self, qname): return lookup_element(qname, self.elements, self.validator.BUILDERS_MAP) def lookup(self, tag, qname): """ General lookup method for XSD global components. :param tag: the expanded QName of the XSD the global declaration/definition \ (eg. '{http://www.w3.org/2001/XMLSchema}element'), that is used to select \ the global map for lookup. :param qname: the expanded QName of the component to be looked-up. :returns: an XSD global component. :raises: an XMLSchemaValueError if the *tag* argument is not appropriate for a global \ component, an XMLSchemaKeyError if the *qname* argument is not found in the global map. """ if tag in (XSD_SIMPLE_TYPE, XSD_COMPLEX_TYPE): return self.lookup_type(qname) elif tag == XSD_ELEMENT: return self.lookup_element(qname) elif tag == XSD_GROUP: return self.lookup_group(qname) elif tag == XSD_ATTRIBUTE: return self.lookup_attribute(qname) elif tag == XSD_ATTRIBUTE_GROUP: return self.lookup_attribute_group(qname) elif tag == XSD_NOTATION: return self.lookup_notation(qname) else: raise XMLSchemaValueError("wrong tag {!r} for an XSD global definition/declaration".format(tag)) def get_instance_type(self, type_name, base_type, namespaces): """ Returns the instance XSI type from global maps, validating it with the reference base type. :param type_name: the XSI type attribute value, a QName in prefixed format. :param base_type: the XSD from which the instance type has to be derived. :param namespaces: a map from prefixes to namespaces. """ if base_type.is_complex() and XSI_TYPE in base_type.attributes: base_type.attributes[XSI_TYPE].validate(type_name) extended_name = qname_to_extended(type_name, namespaces) xsi_type = lookup_type(extended_name, self.types, self.validator.BUILDERS_MAP) if not xsi_type.is_derived(base_type): raise XMLSchemaTypeError("%r is not a derived type of %r" % (xsi_type, self)) return xsi_type @property def built(self): return all(schema.built for schema in self.iter_schemas()) @property def unbuilt(self): """Property that returns a list with unbuilt components.""" return [c for s in self.iter_schemas() for c in s.iter_components() if c is not s and not c.built] @property def validation_attempted(self): if self.built: return 'full' elif any(schema.validation_attempted == 'partial' for schema in self.iter_schemas()): return 'partial' else: return 'none' @property def validity(self): if not self.namespaces: return False if all(schema.validity == 'valid' for schema in self.iter_schemas()): return 'valid' elif any(schema.validity == 'invalid' for schema in self.iter_schemas()): return 'invalid' else: return 'notKnown' @property def xsd_version(self): return self.validator.XSD_VERSION @property def builders_map(self): return self.validator.BUILDERS_MAP @property def all_errors(self): errors = [] for schema in self.iter_schemas(): errors.extend(schema.all_errors) return errors @property def constraints(self): """ Old reference to identity constraints, for backward compatibility. Will be removed in v1.1.0. """ return self.identities def iter_components(self, xsd_classes=None): if xsd_classes is None or isinstance(self, xsd_classes): yield self for xsd_global in self.iter_globals(): for obj in xsd_global.iter_components(xsd_classes): yield obj def iter_schemas(self): """Creates an iterator for the schemas registered in the instance.""" for ns_schemas in self.namespaces.values(): for schema in ns_schemas: yield schema def iter_globals(self): """ Creates an iterator for XSD global definitions/declarations. """ for global_map in self.global_maps: for obj in global_map.values(): yield obj def register(self, schema): """ Registers an XMLSchema instance. """ try: ns_schemas = self.namespaces[schema.target_namespace] except KeyError: self.namespaces[schema.target_namespace] = [schema] else: if schema in ns_schemas: return elif not any(schema.url == obj.url and schema.__class__ == obj.__class__ for obj in ns_schemas): ns_schemas.append(schema) def clear(self, remove_schemas=False, only_unbuilt=False): """ Clears the instance maps and schemas. :param remove_schemas: removes also the schema instances. :param only_unbuilt: removes only not built objects/schemas. """ if only_unbuilt: not_built_schemas = {schema for schema in self.iter_schemas() if not schema.built} if not not_built_schemas: return for global_map in self.global_maps: for k in list(global_map.keys()): obj = global_map[k] if not isinstance(obj, XsdComponent) or obj.schema in not_built_schemas: del global_map[k] if k in self.substitution_groups: del self.substitution_groups[k] if k in self.identities: del self.identities[k] if remove_schemas: namespaces = NamespaceResourcesMap() for uri, value in self.namespaces.items(): for schema in value: if schema not in not_built_schemas: namespaces[uri] = schema self.namespaces = namespaces else: for global_map in self.global_maps: global_map.clear() self.substitution_groups.clear() self.identities.clear() if remove_schemas: self.namespaces.clear() def build(self): """ Build the maps of XSD global definitions/declarations. The global maps are updated adding and building the globals of not built registered schemas. """ try: meta_schema = self.namespaces[XSD_NAMESPACE][0] except KeyError: # Meta-schemas are not registered. If any of base namespaces is already registered # create a new meta-schema, otherwise register the meta-schemas. meta_schema = self.validator.meta_schema if meta_schema is None: raise XMLSchemaValueError("{!r} has not a meta-schema".format(self.validator)) if any(ns in self.namespaces for ns in meta_schema.BASE_SCHEMAS): base_schemas = {k: v for k, v in meta_schema.BASE_SCHEMAS.items() if k not in self.namespaces} meta_schema = self.validator.create_meta_schema(meta_schema.url, base_schemas, self) for schema in self.iter_schemas(): if schema.meta_schema is not None: schema.meta_schema = meta_schema else: for schema in meta_schema.maps.iter_schemas(): self.register(schema) self.types.update(meta_schema.maps.types) self.attributes.update(meta_schema.maps.attributes) self.attribute_groups.update(meta_schema.maps.attribute_groups) self.groups.update(meta_schema.maps.groups) self.notations.update(meta_schema.maps.notations) self.elements.update(meta_schema.maps.elements) self.substitution_groups.update(meta_schema.maps.substitution_groups) self.identities.update(meta_schema.maps.identities) not_built_schemas = [schema for schema in self.iter_schemas() if not schema.built] for schema in not_built_schemas: schema._root_elements = None # Load and build global declarations load_xsd_simple_types(self.types, not_built_schemas) load_xsd_complex_types(self.types, not_built_schemas) load_xsd_notations(self.notations, not_built_schemas) load_xsd_attributes(self.attributes, not_built_schemas) load_xsd_attribute_groups(self.attribute_groups, not_built_schemas) load_xsd_elements(self.elements, not_built_schemas) load_xsd_groups(self.groups, not_built_schemas) if not meta_schema.built: xsd_builtin_types_factory(meta_schema, self.types) for qname in self.notations: self.lookup_notation(qname) for qname in self.attributes: self.lookup_attribute(qname) for qname in self.attribute_groups: self.lookup_attribute_group(qname) for schema in filter( lambda x: isinstance(x.default_attributes, string_base_type), not_built_schemas): try: schema.default_attributes = schema.maps.attribute_groups[schema.default_attributes] except KeyError: schema.default_attributes = None msg = "defaultAttributes={!r} doesn't match an attribute group of {!r}" schema.parse_error( error=msg.format(schema.root.get('defaultAttributes'), schema), elem=schema.root, validation=schema.validation ) for qname in self.types: self.lookup_type(qname) for qname in self.elements: self.lookup_element(qname) for qname in self.groups: self.lookup_group(qname) # Builds element declarations inside model groups. for schema in not_built_schemas: for group in schema.iter_components(XsdGroup): group.build() # Builds xs:keyref's key references for constraint in filter(lambda x: isinstance(x, XsdKeyref), self.identities.values()): constraint.parse_refer() # Build XSD 1.1 identity references and assertions if self.xsd_version != '1.0': for schema in filter(lambda x: x.meta_schema is not None, not_built_schemas): for e in schema.iter_components(Xsd11Element): for constraint in filter(lambda x: x.ref is not None, e.identities.values()): try: ref = self.identities[constraint.name] except KeyError: schema.parse_error("Unknown %r constraint %r" % (type(constraint), constraint.name)) else: constraint.selector = ref.selector constraint.fields = ref.fields if not isinstance(ref, constraint.__class__): constraint.parse_error("attribute 'ref' points to a different kind constraint") elif isinstance(constraint, XsdKeyref): constraint.refer = ref.refer constraint.ref = ref for assertion in schema.iter_components(XsdAssert): assertion.parse_xpath_test() self.check(filter(lambda x: x.meta_schema is not None, not_built_schemas), self.validation) def check(self, schemas=None, validation='strict'): """ Checks the global maps. For default checks all schemas and raises an exception at first error. :param schemas: optional argument with the set of the schemas to check. :param validation: overrides the default validation mode of the validator. :raise: XMLSchemaParseError """ schemas = set(schemas if schemas is not None else self.iter_schemas()) # Checks substitution groups circularities for qname in self.substitution_groups: xsd_element = self.elements[qname] for e in xsd_element.iter_substitutes(): if e is xsd_element: msg = "circularity found for substitution group with head element %r" e.parse_error(msg.format(e), validation=validation) elif e.abstract and e.name not in self.substitution_groups and self.xsd_version > '1.0': self.parse_error("in XSD 1.1 an abstract element cannot be member of a substitution group") if validation == 'strict' and not self.built: raise XMLSchemaNotBuiltError(self, "global map has unbuilt components: %r" % self.unbuilt) # Check redefined global groups restrictions for group in filter(lambda x: x.schema in schemas and x.redefine is not None, self.groups.values()): if not any(isinstance(e, XsdGroup) and e.name == group.name for e in group) \ and not group.is_restriction(group.redefine): msg = "The redefined group is an illegal restriction of the original group." group.parse_error(msg, validation=validation) # Check complex content types models restrictions for xsd_global in filter(lambda x: x.schema in schemas, self.iter_globals()): for xsd_type in xsd_global.iter_components(XsdComplexType): if not isinstance(xsd_type.content_type, XsdGroup): continue if xsd_type.derivation == 'restriction': base_type = xsd_type.base_type if base_type and base_type.name != XSD_ANY_TYPE and base_type.is_complex(): if not xsd_type.content_type.is_restriction(base_type.content_type): msg = "The derived group is an illegal restriction of the base type group." xsd_type.parse_error(msg, validation=validation) if base_type.is_complex() and not base_type.open_content and \ xsd_type.open_content and xsd_type.open_content.mode != 'none': group = xsd_type.schema.create_any_content_group( parent=xsd_type, any_element=xsd_type.open_content.any_element ) if not group.is_restriction(base_type.content_type): self.parse_error("restriction has an open content but base type has not") try: xsd_type.content_type.check_model() except XMLSchemaModelDepthError: msg = "cannot verify the content model of {!r} due to maximum recursion depth exceeded" xsd_type.schema.warnings.append(msg.format(xsd_type)) warnings.warn(msg, XMLSchemaWarning, stacklevel=4) except XMLSchemaModelError as err: if validation == 'strict': raise xsd_type.errors.append(err)