1# This program is free software; you can redistribute it and/or modify it under
2# the terms of the (LGPL) GNU Lesser General Public License as published by the
3# Free Software Foundation; either version 3 of the License, or (at your
4# option) any later version.
5#
6# This program is distributed in the hope that it will be useful, but WITHOUT
7# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
8# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
9# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
10#
11# You should have received a copy of the GNU Lesser General Public License
12# along with this program; if not, write to the Free Software Foundation, Inc.,
13# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
14# written by: Jeff Ortel ( jortel@redhat.com )
15
16"""Classes representing I{basic} XSD schema objects."""
17
18from suds import *
19from suds.reader import DocumentReader
20from suds.sax import Namespace
21from suds.transport import TransportError
22from suds.xsd import *
23from suds.xsd.query import *
24from suds.xsd.sxbase import *
25
26from urllib.parse import urljoin
27
28from logging import getLogger
29log = getLogger(__name__)
30
31
32class RestrictionMatcher:
33    """For use with L{NodeFinder} to match restriction."""
34    def match(self, n):
35        return isinstance(n, Restriction)
36
37
38class TypedContent(Content):
39    """Represents any I{typed} content."""
40
41    def __init__(self, *args, **kwargs):
42        Content.__init__(self, *args, **kwargs)
43        self.resolved_cache = {}
44
45    def resolve(self, nobuiltin=False):
46        """
47        Resolve the node's type reference and return the referenced type node.
48
49        Returns self if the type is defined locally, e.g. as a <complexType>
50        subnode. Otherwise returns the referenced external node.
51
52        @param nobuiltin: Flag indicating whether resolving to XSD built-in
53            types should not be allowed.
54        @return: The resolved (true) type.
55        @rtype: L{SchemaObject}
56
57        """
58        cached = self.resolved_cache.get(nobuiltin)
59        if cached is not None:
60            return cached
61        resolved = self.__resolve_type(nobuiltin)
62        self.resolved_cache[nobuiltin] = resolved
63        return resolved
64
65    def __resolve_type(self, nobuiltin=False):
66        """
67        Private resolve() worker without any result caching.
68
69        @param nobuiltin: Flag indicating whether resolving to XSD built-in
70            types should not be allowed.
71        @return: The resolved (true) type.
72        @rtype: L{SchemaObject}
73
74        """
75        # There is no need for a recursive implementation here since a node can
76        # reference an external type node but XSD specification explicitly
77        # states that that external node must not be a reference to yet another
78        # node.
79        qref = self.qref()
80        if qref is None:
81            return self
82        query = TypeQuery(qref)
83        query.history = [self]
84        log.debug("%s, resolving: %s\n using:%s", self.id, qref, query)
85        resolved = query.execute(self.schema)
86        if resolved is None:
87            log.debug(self.schema)
88            raise TypeNotFound(qref)
89        if resolved.builtin() and nobuiltin:
90            return self
91        return resolved
92
93    def qref(self):
94        """
95        Get the I{type} qualified reference to the referenced XSD type.
96
97        This method takes into account simple types defined through restriction
98        which are detected by determining that self is simple (len == 0) and by
99        finding a restriction child.
100
101        @return: The I{type} qualified reference.
102        @rtype: qref
103
104        """
105        qref = self.type
106        if qref is None and len(self) == 0:
107            ls = []
108            m = RestrictionMatcher()
109            finder = NodeFinder(m, 1)
110            finder.find(self, ls)
111            if ls:
112                return ls[0].ref
113        return qref
114
115
116class Complex(SchemaObject):
117    """
118    Represents an XSD schema <xsd:complexType/> node.
119
120    @cvar childtags: A list of valid child node names.
121    @type childtags: (I{str},...)
122
123    """
124
125    def childtags(self):
126        return ("all", "any", "attribute", "attributeGroup", "choice",
127            "complexContent", "group", "sequence", "simpleContent")
128
129    def description(self):
130        return ("name",)
131
132    def extension(self):
133        for c in self.rawchildren:
134            if c.extension():
135                return True
136        return False
137
138    def mixed(self):
139        for c in self.rawchildren:
140            if isinstance(c, SimpleContent) and c.mixed():
141                return True
142        return False
143
144
145class Group(SchemaObject):
146    """
147    Represents an XSD schema <xsd:group/> node.
148
149    @cvar childtags: A list of valid child node names.
150    @type childtags: (I{str},...)
151
152    """
153
154    def childtags(self):
155        return "all", "choice", "sequence"
156
157    def dependencies(self):
158        deps = []
159        midx = None
160        if self.ref is not None:
161            query = GroupQuery(self.ref)
162            g = query.execute(self.schema)
163            if g is None:
164                log.debug(self.schema)
165                raise TypeNotFound(self.ref)
166            deps.append(g)
167            midx = 0
168        return midx, deps
169
170    def merge(self, other):
171        SchemaObject.merge(self, other)
172        self.rawchildren = other.rawchildren
173
174    def description(self):
175        return "name", "ref"
176
177
178class AttributeGroup(SchemaObject):
179    """
180    Represents an XSD schema <xsd:attributeGroup/> node.
181
182    @cvar childtags: A list of valid child node names.
183    @type childtags: (I{str},...)
184
185    """
186
187    def childtags(self):
188        return "attribute", "attributeGroup"
189
190    def dependencies(self):
191        deps = []
192        midx = None
193        if self.ref is not None:
194            query = AttrGroupQuery(self.ref)
195            ag = query.execute(self.schema)
196            if ag is None:
197                log.debug(self.schema)
198                raise TypeNotFound(self.ref)
199            deps.append(ag)
200            midx = 0
201        return midx, deps
202
203    def merge(self, other):
204        SchemaObject.merge(self, other)
205        self.rawchildren = other.rawchildren
206
207    def description(self):
208        return "name", "ref"
209
210
211class Simple(SchemaObject):
212    """Represents an XSD schema <xsd:simpleType/> node."""
213
214    def childtags(self):
215        return "any", "list", "restriction"
216
217    def enum(self):
218        for child, ancestry in self.children():
219            if isinstance(child, Enumeration):
220                return True
221        return False
222
223    def mixed(self):
224        return len(self)
225
226    def description(self):
227        return ("name",)
228
229    def extension(self):
230        for c in self.rawchildren:
231            if c.extension():
232                return True
233        return False
234
235    def restriction(self):
236        for c in self.rawchildren:
237            if c.restriction():
238                return True
239        return False
240
241
242class List(SchemaObject):
243    """Represents an XSD schema <xsd:list/> node."""
244
245    def childtags(self):
246        return ()
247
248    def description(self):
249        return ("name",)
250
251    def xslist(self):
252        return True
253
254
255class Restriction(SchemaObject):
256    """Represents an XSD schema <xsd:restriction/> node."""
257
258    def __init__(self, schema, root):
259        SchemaObject.__init__(self, schema, root)
260        self.ref = root.get("base")
261
262    def childtags(self):
263        return "attribute", "attributeGroup", "enumeration"
264
265    def dependencies(self):
266        deps = []
267        midx = None
268        if self.ref is not None:
269            query = TypeQuery(self.ref)
270            super = query.execute(self.schema)
271            if super is None:
272                log.debug(self.schema)
273                raise TypeNotFound(self.ref)
274            if not super.builtin():
275                deps.append(super)
276                midx = 0
277        return midx, deps
278
279    def restriction(self):
280        return True
281
282    def merge(self, other):
283        SchemaObject.merge(self, other)
284        filter = Filter(False, self.rawchildren)
285        self.prepend(self.rawchildren, other.rawchildren, filter)
286
287    def description(self):
288        return ("ref",)
289
290
291class Collection(SchemaObject):
292    """Represents an XSD schema collection (a.k.a. order indicator) node."""
293
294    def childtags(self):
295        return "all", "any", "choice", "element", "group", "sequence"
296
297
298class All(Collection):
299    """Represents an XSD schema <xsd:all/> node."""
300    def all(self):
301        return True
302
303
304class Choice(Collection):
305    """Represents an XSD schema <xsd:choice/> node."""
306    def choice(self):
307        return True
308
309
310class Sequence(Collection):
311    """Represents an XSD schema <xsd:sequence/> node."""
312    def sequence(self):
313        return True
314
315
316class ComplexContent(SchemaObject):
317    """Represents an XSD schema <xsd:complexContent/> node."""
318
319    def childtags(self):
320        return "attribute", "attributeGroup", "extension", "restriction"
321
322    def extension(self):
323        for c in self.rawchildren:
324            if c.extension():
325                return True
326        return False
327
328    def restriction(self):
329        for c in self.rawchildren:
330            if c.restriction():
331                return True
332        return False
333
334
335class SimpleContent(SchemaObject):
336    """Represents an XSD schema <xsd:simpleContent/> node."""
337
338    def childtags(self):
339        return "extension", "restriction"
340
341    def extension(self):
342        for c in self.rawchildren:
343            if c.extension():
344                return True
345        return False
346
347    def restriction(self):
348        for c in self.rawchildren:
349            if c.restriction():
350                return True
351        return False
352
353    def mixed(self):
354        return len(self)
355
356
357class Enumeration(Content):
358    """Represents an XSD schema <xsd:enumeration/> node."""
359
360    def __init__(self, schema, root):
361        Content.__init__(self, schema, root)
362        self.name = root.get("value")
363
364    def description(self):
365        return ("name",)
366
367    def enum(self):
368        return True
369
370
371class Element(TypedContent):
372    """Represents an XSD schema <xsd:element/> node."""
373
374    def __init__(self, schema, root):
375        TypedContent.__init__(self, schema, root)
376        is_reference = self.ref is not None
377        is_top_level = root.parent is schema.root
378        if is_reference or is_top_level:
379            self.form_qualified = True
380        else:
381            form = root.get("form")
382            if form is not None:
383                self.form_qualified = (form == "qualified")
384        nillable = self.root.get("nillable")
385        if nillable is not None:
386            self.nillable = (nillable in ("1", "true"))
387        self.implany()
388
389    def implany(self):
390        """
391        Set the type to <xsd:any/> when implicit.
392
393        An element has an implicit <xsd:any/> type when it has no body and no
394        explicitly defined type.
395
396        @return: self
397        @rtype: L{Element}
398
399        """
400        if self.type is None and self.ref is None and self.root.isempty():
401            self.type = self.anytype()
402
403    def childtags(self):
404        return "any", "attribute", "complexType", "simpleType"
405
406    def extension(self):
407        for c in self.rawchildren:
408            if c.extension():
409                return True
410        return False
411
412    def restriction(self):
413        for c in self.rawchildren:
414            if c.restriction():
415                return True
416        return False
417
418    def dependencies(self):
419        deps = []
420        midx = None
421        e = self.__deref()
422        if e is not None:
423            deps.append(e)
424            midx = 0
425        return midx, deps
426
427    def merge(self, other):
428        SchemaObject.merge(self, other)
429        self.rawchildren = other.rawchildren
430
431    def description(self):
432        return "name", "ref", "type"
433
434    def anytype(self):
435        """Create an xsd:anyType reference."""
436        p, u = Namespace.xsdns
437        mp = self.root.findPrefix(u)
438        if mp is None:
439            mp = p
440            self.root.addPrefix(p, u)
441        return ":".join((mp, "anyType"))
442
443    def namespace(self, prefix=None):
444        """
445        Get this schema element's target namespace.
446
447        In case of reference elements, the target namespace is defined by the
448        referenced and not the referencing element node.
449
450        @param prefix: The default prefix.
451        @type prefix: str
452        @return: The schema element's target namespace
453        @rtype: (I{prefix},I{URI})
454
455        """
456        e = self.__deref()
457        if e is not None:
458            return e.namespace(prefix)
459        return super(Element, self).namespace()
460
461    def __deref(self):
462        if self.ref is None:
463            return
464        query = ElementQuery(self.ref)
465        e = query.execute(self.schema)
466        if e is None:
467            log.debug(self.schema)
468            raise TypeNotFound(self.ref)
469        return e
470
471
472class Extension(SchemaObject):
473    """Represents an XSD schema <xsd:extension/> node."""
474
475    def __init__(self, schema, root):
476        SchemaObject.__init__(self, schema, root)
477        self.ref = root.get("base")
478
479    def childtags(self):
480        return ("all", "attribute", "attributeGroup", "choice", "group",
481            "sequence")
482
483    def dependencies(self):
484        deps = []
485        midx = None
486        if self.ref is not None:
487            query = TypeQuery(self.ref)
488            super = query.execute(self.schema)
489            if super is None:
490                log.debug(self.schema)
491                raise TypeNotFound(self.ref)
492            if not super.builtin():
493                deps.append(super)
494                midx = 0
495        return midx, deps
496
497    def merge(self, other):
498        SchemaObject.merge(self, other)
499        filter = Filter(False, self.rawchildren)
500        self.prepend(self.rawchildren, other.rawchildren, filter)
501
502    def extension(self):
503        return self.ref is not None
504
505    def description(self):
506        return ("ref",)
507
508
509class Import(SchemaObject):
510    """
511    Represents an XSD schema <xsd:import/> node.
512
513    @cvar locations: A dictionary of namespace locations.
514    @type locations: dict
515    @ivar ns: The imported namespace.
516    @type ns: str
517    @ivar location: The (optional) location.
518    @type location: namespace-uri
519    @ivar opened: Opened and I{imported} flag.
520    @type opened: boolean
521
522    """
523
524    locations = {}
525
526    @classmethod
527    def bind(cls, ns, location=None):
528        """
529        Bind a namespace to a schema location (URI).
530
531        This is used for imports that do not specify a schemaLocation.
532
533        @param ns: A namespace-uri.
534        @type ns: str
535        @param location: The (optional) schema location for the namespace.
536            (default=ns)
537        @type location: str
538
539        """
540        if location is None:
541            location = ns
542        cls.locations[ns] = location
543
544    def __init__(self, schema, root):
545        SchemaObject.__init__(self, schema, root)
546        self.ns = (None, root.get("namespace"))
547        self.location = root.get("schemaLocation")
548        if self.location is None:
549            self.location = self.locations.get(self.ns[1])
550        self.opened = False
551
552    def open(self, options, loaded_schemata):
553        """
554        Open and import the referenced schema.
555
556        @param options: An options dictionary.
557        @type options: L{options.Options}
558        @param loaded_schemata: Already loaded schemata cache (URL --> Schema).
559        @type loaded_schemata: dict
560        @return: The referenced schema.
561        @rtype: L{Schema}
562
563        """
564        if self.opened:
565            return
566        self.opened = True
567        log.debug("%s, importing ns='%s', location='%s'", self.id, self.ns[1],
568            self.location)
569        result = self.__locate()
570        if result is None:
571            if self.location is None:
572                log.debug("imported schema (%s) not-found", self.ns[1])
573            else:
574                url = self.location
575                if "://" not in url:
576                    url = urljoin(self.schema.baseurl, url)
577                result = (loaded_schemata.get(url) or
578                    self.__download(url, loaded_schemata, options))
579        log.debug("imported:\n%s", result)
580        return result
581
582    def __locate(self):
583        """Find the schema locally."""
584        if self.ns[1] != self.schema.tns[1]:
585            return self.schema.locate(self.ns)
586
587    def __download(self, url, loaded_schemata, options):
588        """Download the schema."""
589        try:
590            reader = DocumentReader(options)
591            d = reader.open(url)
592            root = d.root()
593            root.set("url", url)
594            return self.schema.instance(root, url, loaded_schemata, options)
595        except TransportError:
596            msg = "import schema (%s) at (%s), failed" % (self.ns[1], url)
597            log.error("%s, %s", self.id, msg, exc_info=True)
598            raise Exception(msg)
599
600    def description(self):
601        return "ns", "location"
602
603
604class Include(SchemaObject):
605    """
606    Represents an XSD schema <xsd:include/> node.
607
608    @ivar location: The (optional) location.
609    @type location: namespace-uri
610    @ivar opened: Opened and I{imported} flag.
611    @type opened: boolean
612
613    """
614
615    locations = {}
616
617    def __init__(self, schema, root):
618        SchemaObject.__init__(self, schema, root)
619        self.location = root.get("schemaLocation")
620        if self.location is None:
621            self.location = self.locations.get(self.ns[1])
622        self.opened = False
623
624    def open(self, options, loaded_schemata):
625        """
626        Open and include the referenced schema.
627
628        @param options: An options dictionary.
629        @type options: L{options.Options}
630        @param loaded_schemata: Already loaded schemata cache (URL --> Schema).
631        @type loaded_schemata: dict
632        @return: The referenced schema.
633        @rtype: L{Schema}
634
635        """
636        if self.opened:
637            return
638        self.opened = True
639        log.debug("%s, including location='%s'", self.id, self.location)
640        url = self.location
641        if "://" not in url:
642            url = urljoin(self.schema.baseurl, url)
643        result = (loaded_schemata.get(url) or
644            self.__download(url, loaded_schemata, options))
645        log.debug("included:\n%s", result)
646        return result
647
648    def __download(self, url, loaded_schemata, options):
649        """Download the schema."""
650        try:
651            reader = DocumentReader(options)
652            d = reader.open(url)
653            root = d.root()
654            root.set("url", url)
655            self.__applytns(root)
656            return self.schema.instance(root, url, loaded_schemata, options)
657        except TransportError:
658            msg = "include schema at (%s), failed" % url
659            log.error("%s, %s", self.id, msg, exc_info=True)
660            raise Exception(msg)
661
662    def __applytns(self, root):
663        """Make sure included schema has the same target namespace."""
664        TNS = "targetNamespace"
665        tns = root.get(TNS)
666        if tns is None:
667            tns = self.schema.tns[1]
668            root.set(TNS, tns)
669        else:
670            if self.schema.tns[1] != tns:
671                raise Exception("%s mismatch" % TNS)
672
673    def description(self):
674        return "location"
675
676
677class Attribute(TypedContent):
678    """Represents an XSD schema <attribute/> node."""
679
680    def __init__(self, schema, root):
681        TypedContent.__init__(self, schema, root)
682        self.use = root.get("use", default="")
683
684    def childtags(self):
685        return ("restriction",)
686
687    def isattr(self):
688        return True
689
690    def get_default(self):
691        """
692        Gets the <xsd:attribute default=""/> attribute value.
693
694        @return: The default value for the attribute
695        @rtype: str
696
697        """
698        return self.root.get("default", default="")
699
700    def optional(self):
701        return self.use != "required"
702
703    def dependencies(self):
704        deps = []
705        midx = None
706        if self.ref is not None:
707            query = AttrQuery(self.ref)
708            a = query.execute(self.schema)
709            if a is None:
710                log.debug(self.schema)
711                raise TypeNotFound(self.ref)
712            deps.append(a)
713            midx = 0
714        return midx, deps
715
716    def description(self):
717        return "name", "ref", "type"
718
719
720class Any(Content):
721    """Represents an XSD schema <any/> node."""
722
723    def get_child(self, name):
724        root = self.root.clone()
725        root.set("note", "synthesized (any) child")
726        child = Any(self.schema, root)
727        return child, []
728
729    def get_attribute(self, name):
730        root = self.root.clone()
731        root.set("note", "synthesized (any) attribute")
732        attribute = Any(self.schema, root)
733        return attribute, []
734
735    def any(self):
736        return True
737
738
739class Factory:
740    """
741    @cvar tags: A factory to create object objects based on tag.
742    @type tags: {tag:fn,}
743
744    """
745
746    tags = {
747        "all": All,
748        "any": Any,
749        "attribute": Attribute,
750        "attributeGroup": AttributeGroup,
751        "choice": Choice,
752        "complexContent": ComplexContent,
753        "complexType": Complex,
754        "element": Element,
755        "enumeration": Enumeration,
756        "extension": Extension,
757        "group": Group,
758        "import": Import,
759        "include": Include,
760        "list": List,
761        "restriction": Restriction,
762        "simpleContent": SimpleContent,
763        "simpleType": Simple,
764        "sequence": Sequence,
765    }
766
767    @classmethod
768    def maptag(cls, tag, fn):
769        """
770        Map (override) tag => I{class} mapping.
771
772        @param tag: An XSD tag name.
773        @type tag: str
774        @param fn: A function or class.
775        @type fn: fn|class.
776
777        """
778        cls.tags[tag] = fn
779
780    @classmethod
781    def create(cls, root, schema):
782        """
783        Create an object based on the root tag name.
784
785        @param root: An XML root element.
786        @type root: L{Element}
787        @param schema: A schema object.
788        @type schema: L{schema.Schema}
789        @return: The created object.
790        @rtype: L{SchemaObject}
791
792        """
793        fn = cls.tags.get(root.name)
794        if fn is not None:
795            return fn(schema, root)
796
797    @classmethod
798    def build(cls, root, schema, filter=("*",)):
799        """
800        Build an xsobject representation.
801
802        @param root: An schema XML root.
803        @type root: L{sax.element.Element}
804        @param filter: A tag filter.
805        @type filter: [str,...]
806        @return: A schema object graph.
807        @rtype: L{sxbase.SchemaObject}
808
809        """
810        children = []
811        for node in root.getChildren(ns=Namespace.xsdns):
812            if "*" in filter or node.name in filter:
813                child = cls.create(node, schema)
814                if child is None:
815                    continue
816                children.append(child)
817                c = cls.build(node, schema, child.childtags())
818                child.rawchildren = c
819        return children
820
821    @classmethod
822    def collate(cls, children):
823        imports = []
824        elements = {}
825        attributes = {}
826        types = {}
827        groups = {}
828        agrps = {}
829        for c in children:
830            if isinstance(c, (Import, Include)):
831                imports.append(c)
832                continue
833            if isinstance(c, Attribute):
834                attributes[c.qname] = c
835                continue
836            if isinstance(c, Element):
837                elements[c.qname] = c
838                continue
839            if isinstance(c, Group):
840                groups[c.qname] = c
841                continue
842            if isinstance(c, AttributeGroup):
843                agrps[c.qname] = c
844                continue
845            types[c.qname] = c
846        for i in imports:
847            children.remove(i)
848        return children, imports, attributes, elements, types, groups, agrps
849
850
851#######################################################
852# Static Import Bindings :-(
853#######################################################
854Import.bind(
855    "http://schemas.xmlsoap.org/soap/encoding/",
856    "suds://schemas.xmlsoap.org/soap/encoding/")
857Import.bind(
858    "http://www.w3.org/XML/1998/namespace",
859    "http://www.w3.org/2001/xml.xsd")
860Import.bind(
861    "http://www.w3.org/2001/XMLSchema",
862    "http://www.w3.org/2001/XMLSchema.xsd")
863