1# This program is free software; you can redistribute it and/or modify
2# it under the terms of the (LGPL) GNU Lesser General Public License as
3# published by the Free Software Foundation; either version 3 of the
4# License, or (at your option) any later version.
5#
6# This program is distributed in the hope that it will be useful,
7# but WITHOUT ANY WARRANTY; without even the implied warranty of
8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9# GNU Library Lesser General Public License for more details at
10# ( http://www.gnu.org/licenses/lgpl.html ).
11#
12# You should have received a copy of the GNU Lesser General Public License
13# along with this program; if not, write to the Free Software
14# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
15# written by: Jeff Ortel ( jortel@redhat.com )
16
17"""
18The I{sxbase} module provides I{base} classes representing schema objects.
19"""
20
21from suds import *
22from suds.xsd import *
23from suds.sax.element import Element
24from suds.sax import Namespace
25
26from logging import getLogger
27log = getLogger(__name__)
28
29
30class SchemaObject(UnicodeMixin):
31    """
32    A schema object is an extension to object with schema awareness.
33    @ivar root: The XML root element.
34    @type root: L{Element}
35    @ivar schema: The schema containing this object.
36    @type schema: L{schema.Schema}
37    @ivar form_qualified: A flag that indicates that @elementFormDefault
38        has a value of I{qualified}.
39    @type form_qualified: boolean
40    @ivar nillable: A flag that indicates that @nillable
41        has a value of I{true}.
42    @type nillable: boolean
43    @ivar default: The default value.
44    @type default: object
45    @ivar rawchildren: A list raw of all children.
46    @type rawchildren: [L{SchemaObject},...]
47    """
48
49    @classmethod
50    def prepend(cls, d, s, filter=Filter()):
51        """
52        Prepend schema objects from B{s}ource list to
53        the B{d}estination list while applying the filter.
54        @param d: The destination list.
55        @type d: list
56        @param s: The source list.
57        @type s: list
58        @param filter: A filter that allows items to be prepended.
59        @type filter: L{Filter}
60        """
61        i = 0
62        for x in s:
63            if x in filter:
64                d.insert(i, x)
65                i += 1
66
67    @classmethod
68    def append(cls, d, s, filter=Filter()):
69        """
70        Append schema objects from B{s}ource list to
71        the B{d}estination list while applying the filter.
72        @param d: The destination list.
73        @type d: list
74        @param s: The source list.
75        @type s: list
76        @param filter: A filter that allows items to be appended.
77        @type filter: L{Filter}
78        """
79        for item in s:
80            if item in filter:
81                d.append(item)
82
83    def __init__(self, schema, root):
84        """
85        @param schema: The containing schema.
86        @type schema: L{schema.Schema}
87        @param root: The XML root node.
88        @type root: L{Element}
89        """
90        self.schema = schema
91        self.root = root
92        self.id = objid(self)
93        self.name = root.get('name')
94        self.qname = (self.name, schema.tns[1])
95        self.min = root.get('minOccurs')
96        self.max = root.get('maxOccurs')
97        self.type = root.get('type')
98        self.ref = root.get('ref')
99        self.form_qualified = schema.form_qualified
100        self.nillable = False
101        self.default = root.get('default')
102        self.rawchildren = []
103
104    def attributes(self, filter=Filter()):
105        """
106        Get only the attribute content.
107        @param filter: A filter to constrain the result.
108        @type filter: L{Filter}
109        @return: A list of tuples (attr, ancestry)
110        @rtype: [(L{SchemaObject}, [L{SchemaObject},..]),..]
111        """
112        result = []
113        for child, ancestry in self:
114            if child.isattr() and child in filter:
115                result.append((child, ancestry))
116        return result
117
118    def children(self, filter=Filter()):
119        """
120        Get only the I{direct} or non-attribute content.
121        @param filter: A filter to constrain the result.
122        @type filter: L{Filter}
123        @return: A list tuples: (child, ancestry)
124        @rtype: [(L{SchemaObject}, [L{SchemaObject},..]),..]
125        """
126        result = []
127        for child, ancestry in self:
128            if not child.isattr() and child in filter:
129                result.append((child, ancestry))
130        return result
131
132    def get_attribute(self, name):
133        """
134        Get (find) an attribute by name.
135        @param name: A attribute name.
136        @type name: str
137        @return: A tuple: the requested (attribute, ancestry).
138        @rtype: (L{SchemaObject}, [L{SchemaObject},..])
139        """
140        for child, ancestry in self.attributes():
141            if child.name == name:
142                return child, ancestry
143        return None, []
144
145    def get_child(self, name):
146        """
147        Get (find) a I{non-attribute} child by name.
148        @param name: A child name.
149        @type name: str
150        @return: A tuple: the requested (child, ancestry).
151        @rtype: (L{SchemaObject}, [L{SchemaObject},..])
152        """
153        for child, ancestry in self.children():
154            if child.any() or child.name == name:
155                return child, ancestry
156        return None, []
157
158    def namespace(self, prefix=None):
159        """
160        Get this property's namespace.
161        @param prefix: The default prefix.
162        @type prefix: str
163        @return: The schema's target namespace
164        @rtype: (I{prefix},I{URI})
165        """
166        ns = self.schema.tns
167        if ns[0] is None:
168            ns = (prefix, ns[1])
169        return ns
170
171    def default_namespace(self):
172        return self.root.defaultNamespace()
173
174    def multi_occurrence(self):
175        """
176        Get whether the node has multiple occurrences, i.e. is a I{collection}.
177        @return: True if it has, False if it has 1 occurrence at most.
178        @rtype: boolean
179        """
180        max = self.max
181        if max is None:
182            return False
183        if max.isdigit():
184            return int(max) > 1
185        return max == 'unbounded'
186
187    def optional(self):
188        """
189        Get whether this type is optional.
190        @return: True if optional, else False
191        @rtype: boolean
192        """
193        return self.min == '0'
194
195    def required(self):
196        """
197        Get whether this type is required.
198        @return: True if required, else False
199        @rtype: boolean
200        """
201        return not self.optional()
202
203    def resolve(self, nobuiltin=False):
204        """
205        Resolve the node's type reference and return the referenced type node.
206
207        Only schema objects that actually support 'having a type' custom
208        implement this interface while others simply resolve as themselves.
209        @param nobuiltin: Flag indicating whether resolving to an external XSD
210            builtin type should not be allowed.
211        @return: The resolved (true) type.
212        @rtype: L{SchemaObject}
213        """
214        return self
215
216    def sequence(self):
217        """
218        Get whether this is a <xs:sequence/>.
219        @return: True if <xs:sequence/>, else False
220        @rtype: boolean
221        """
222        return False
223
224    def xslist(self):
225        """
226        Get whether this is a <xs:list/>.
227        @return: True if any, else False
228        @rtype: boolean
229        """
230        return False
231
232    def all(self):
233        """
234        Get whether this is an <xs:all/>.
235        @return: True if any, else False
236        @rtype: boolean
237        """
238        return False
239
240    def choice(self):
241        """
242        Get whether this is a <xs:choice/>.
243        @return: True if any, else False
244        @rtype: boolean
245        """
246        return False
247
248    def any(self):
249        """
250        Get whether this is an <xs:any/>.
251        @return: True if any, else False
252        @rtype: boolean
253        """
254        return False
255
256    def builtin(self):
257        """
258        Get whether this is a schema-instance (xs) type.
259        @return: True if any, else False
260        @rtype: boolean
261        """
262        return False
263
264    def enum(self):
265        """
266        Get whether this is a simple-type containing an enumeration.
267        @return: True if any, else False
268        @rtype: boolean
269        """
270        return False
271
272    def isattr(self):
273        """
274        Get whether the object is a schema I{attribute} definition.
275        @return: True if an attribute, else False.
276        @rtype: boolean
277        """
278        return False
279
280    def extension(self):
281        """
282        Get whether the object is an extension of another type.
283        @return: True if an extension, else False.
284        @rtype: boolean
285        """
286        return False
287
288    def restriction(self):
289        """
290        Get whether the object is an restriction of another type.
291        @return: True if an restriction, else False.
292        @rtype: boolean
293        """
294        return False
295
296    def mixed(self):
297        """
298        Get whether this I{mixed} content.
299        """
300        return False
301
302    def find(self, qref, classes=()):
303        """
304        Find a referenced type in self or children.
305        @param qref: A qualified reference.
306        @type qref: qref
307        @param classes: A list of classes used to qualify the match.
308        @type classes: [I{class},...]
309        @return: The referenced type.
310        @rtype: L{SchemaObject}
311        @see: L{qualify()}
312        """
313        if not len(classes):
314            classes = (self.__class__,)
315        if self.qname == qref and self.__class__ in classes:
316            return self
317        for c in self.rawchildren:
318            p = c.find(qref, classes)
319            if p is not None:
320                return p
321        return None
322
323    def translate(self, value, topython=True):
324        """
325        Translate a value (type) to/from a Python type.
326        @param value: A value to translate.
327        @return: The converted I{language} type.
328        """
329        return value
330
331    def childtags(self):
332        """
333        Get a list of valid child tag names.
334        @return: A list of child tag names.
335        @rtype: [str,...]
336        """
337        return ()
338
339    def dependencies(self):
340        """
341        Get a list of dependencies for dereferencing.
342        @return: A merge dependency index and a list of dependencies.
343        @rtype: (int, [L{SchemaObject},...])
344        """
345        return None, []
346
347    def autoqualified(self):
348        """
349        The list of I{auto} qualified attribute values.
350        Qualification means to convert values into I{qref}.
351        @return: A list of attibute names.
352        @rtype: list
353        """
354        return ['type', 'ref']
355
356    def qualify(self):
357        """
358        Convert attribute values, that are references to other
359        objects, into I{qref}.  Qualified using the default document namespace.
360        Since many WSDLs are written improperly: when the document does
361        not define its default namespace, the schema target namespace is used
362        to qualify references.
363        """
364        defns = self.root.defaultNamespace()
365        if Namespace.none(defns):
366            defns = self.schema.tns
367        for a in self.autoqualified():
368            ref = getattr(self, a)
369            if ref is None:
370                continue
371            if isqref(ref):
372                continue
373            qref = qualify(ref, self.root, defns)
374            log.debug('%s, convert %s="%s" to %s', self.id, a, ref, qref)
375            setattr(self, a, qref)
376
377    def merge(self, other):
378        """
379        Merge another object as needed.
380        """
381        other.qualify()
382        for n in ('name',
383                  'qname',
384                  'min',
385                  'max',
386                  'default',
387                  'type',
388                  'nillable',
389                  'form_qualified'):
390            if getattr(self, n) is not None:
391                continue
392            v = getattr(other, n)
393            if v is None:
394                continue
395            setattr(self, n, v)
396
397    def content(self, collection=None, filter=Filter(), history=None):
398        """
399        Get a I{flattened} list of this node's contents.
400        @param collection: A list to fill.
401        @type collection: list
402        @param filter: A filter used to constrain the result.
403        @type filter: L{Filter}
404        @param history: The history list used to prevent cyclic dependency.
405        @type history: list
406        @return: The filled list.
407        @rtype: list
408        """
409        if collection is None:
410            collection = []
411        if history is None:
412            history = []
413        if self in history:
414            return collection
415        history.append(self)
416        if self in filter:
417            collection.append(self)
418        for c in self.rawchildren:
419            c.content(collection, filter, history[:])
420        return collection
421
422    def str(self, indent=0, history=None):
423        """
424        Get a string representation of this object.
425        @param indent: The indent.
426        @type indent: int
427        @return: A string.
428        @rtype: str
429        """
430        if history is None:
431            history = []
432        if self in history:
433            return '%s ...' % Repr(self)
434        history.append(self)
435        tab = '%*s'%(indent*3, '')
436        result = ['%s<%s' % (tab, self.id)]
437        for n in self.description():
438            if not hasattr(self, n):
439                continue
440            v = getattr(self, n)
441            if v is None:
442                continue
443            result.append(' %s="%s"' % (n, v))
444        if len(self):
445            result.append('>')
446            for c in self.rawchildren:
447                result.append('\n')
448                result.append(c.str(indent+1, history[:]))
449                if c.isattr():
450                    result.append('@')
451            result.append('\n%s' % tab)
452            result.append('</%s>' % self.__class__.__name__)
453        else:
454            result.append(' />')
455        return ''.join(result)
456
457    def description(self):
458        """
459        Get the names used for str() and repr() description.
460        @return:  A dictionary of relevant attributes.
461        @rtype: [str,...]
462        """
463        return ()
464
465    def __unicode__(self):
466        return unicode(self.str())
467
468    def __repr__(self):
469        s = []
470        s.append('<%s' % self.id)
471        for n in self.description():
472            if not hasattr(self, n):
473                continue
474            v = getattr(self, n)
475            if v is None:
476                continue
477            s.append(' %s="%s"' % (n, v))
478        s.append(' />')
479        return ''.join(s)
480
481    def __len__(self):
482        n = 0
483        for x in self: n += 1
484        return n
485
486    def __iter__(self):
487        return Iter(self)
488
489    def __getitem__(self, index):
490        """Returns a contained schema object referenced by its 0-based index.
491
492        Returns None if such an object does not exist.
493
494        """
495        i = 0
496        for c in self:
497            if i == index:
498                return c
499            i += 1
500
501
502class Iter:
503    """
504    The content iterator - used to iterate the L{Content} children.  The
505    iterator provides a I{view} of the children that is free of container
506    elements such as <sequence/> and <choice/>.
507    @ivar stack: A stack used to control nesting.
508    @type stack: list
509    """
510
511    class Frame:
512        """ A content iterator frame. """
513
514        def __init__(self, sx):
515            """
516            @param sx: A schema object.
517            @type sx: L{SchemaObject}
518            """
519            self.sx = sx
520            self.items = sx.rawchildren
521            self.index = 0
522
523        def next(self):
524            """
525            Get the I{next} item in the frame's collection.
526            @return: The next item or None
527            @rtype: L{SchemaObject}
528            """
529            if self.index < len(self.items):
530                result = self.items[self.index]
531                self.index += 1
532                return result
533
534    def __init__(self, sx):
535        """
536        @param sx: A schema object.
537        @type sx: L{SchemaObject}
538        """
539        self.stack = []
540        self.push(sx)
541
542    def push(self, sx):
543        """
544        Create a frame and push the specified object.
545        @param sx: A schema object to push.
546        @type sx: L{SchemaObject}
547        """
548        self.stack.append(Iter.Frame(sx))
549
550    def pop(self):
551        """
552        Pop the I{top} frame.
553        @return: The popped frame.
554        @rtype: L{Frame}
555        @raise StopIteration: when stack is empty.
556        """
557        if len(self.stack):
558            return self.stack.pop()
559        else:
560            raise StopIteration()
561
562    def top(self):
563        """
564        Get the I{top} frame.
565        @return: The top frame.
566        @rtype: L{Frame}
567        @raise StopIteration: when stack is empty.
568        """
569        if len(self.stack):
570            return self.stack[-1]
571        else:
572            raise StopIteration()
573
574    def next(self):
575        """
576        Get the next item.
577        @return: A tuple: the next (child, ancestry).
578        @rtype: (L{SchemaObject}, [L{SchemaObject},..])
579        @raise StopIteration: A the end.
580        """
581        frame = self.top()
582        while True:
583            result = frame.next()
584            if result is None:
585                self.pop()
586                return self.next()
587            if isinstance(result, Content):
588                ancestry = [f.sx for f in self.stack]
589                return result, ancestry
590            self.push(result)
591            return self.next()
592
593    def __iter__(self):
594        return self
595
596
597class XBuiltin(SchemaObject):
598    """
599    Represents an (XSD) schema <xs:*/> node.
600    """
601
602    def __init__(self, schema, name):
603        """
604        @param schema: The containing schema.
605        @type schema: L{schema.Schema}
606        """
607        root = Element(name)
608        SchemaObject.__init__(self, schema, root)
609        self.name = name
610        self.nillable = True
611
612    def namespace(self, prefix=None):
613        return Namespace.xsdns
614
615    def builtin(self):
616        return True
617
618
619class Content(SchemaObject):
620    """
621    This class represents those schema objects that represent
622    real XML document content.
623    """
624    pass
625
626
627class NodeFinder:
628    """
629    Find nodes based on flexable criteria.  The I{matcher}
630    may be any object that implements a match(n) method.
631    @ivar matcher: An object used as criteria for match.
632    @type matcher: I{any}.match(n)
633    @ivar limit: Limit the number of matches.  0=unlimited.
634    @type limit: int
635    """
636    def __init__(self, matcher, limit=0):
637        """
638        @param matcher: An object used as criteria for match.
639        @type matcher: I{any}.match(n)
640        @param limit: Limit the number of matches.  0=unlimited.
641        @type limit: int
642        """
643        self.matcher = matcher
644        self.limit = limit
645
646    def find(self, node, list):
647        """
648        Traverse the tree looking for matches.
649        @param node: A node to match on.
650        @type node: L{SchemaObject}
651        @param list: A list to fill.
652        @type list: list
653        """
654        if self.matcher.match(node):
655            list.append(node)
656            self.limit -= 1
657            if self.limit == 0:
658                return
659        for c in node.rawchildren:
660            self.find(c, list)
661        return self
662