root/trunk/RBFoundation/RBFoundation/XMLBuilder.py

Revision 670, 11.0 kB (checked in by sholloway, 5 years ago)

*** empty log message ***

Line 
1 #!/usr/bin/env python
2 ##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3 ##~ License
4 ##~
5 ##- The RuneBlade Foundation library is intended to ease some
6 ##- aspects of writing intricate Jabber, XML, and User Interface (wxPython, etc.)
7 ##- applications, while providing the flexibility to modularly change the
8 ##- architecture. Enjoy.
9 ##~
10 ##~ Copyright (C) 2002  TechGame Networks, LLC.
11 ##~
12 ##~ This library is free software; you can redistribute it and/or
13 ##~ modify it under the terms of the BSD style License as found in the
14 ##~ LICENSE file included with this distribution.
15 ##~
16 ##~ TechGame Networks, LLC can be reached at:
17 ##~ 3578 E. Hartsel Drive #211
18 ##~ Colorado Springs, Colorado, USA, 80920
19 ##~
20 ##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
21
22 """Abstract base classes for building Python object trees from an XML stream.
23
24 Classes:
25
26     XMLBuilderObjectBase
27     XMLBuilderMixin
28     XMLBuilder
29
30 History:
31
32     Please see XMLObjectify's history.
33 """
34
35 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
36 #~ Imports                                           
37 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
38
39 from xml.parsers.expat import ParserCreate as _ExpatParserCreate
40 from BindCallable import WeakBindCallable, Curry
41 from XMLNamespaceMap import XMLNamespaceMap
42
43 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
44 #~ Classes
45 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
46
47 class ElementFactoryError(Exception):
48     pass
49
50 class XMLBuilderObjectBase(object):
51     """Base class for objects created by XMLBuilderMixin derived classes."""
52     def __init__(self, owner, parent, node, attributes, namespacemap): pass
53     def _addElement(self, node, obj): pass
54     def _addData(self, data): pass
55     def _xmlInitStarted(self): pass
56     def _xmlInitFinalized(self): pass
57     def _xmlInitComplete(self): pass
58     def _xmlGetElement(self): return self
59     def _xmlChildFactory(self, owner, parent, node, attributes, namespacemap): return None
60     def _toXML(self, strSplit='', *args, **kw):
61         if strSplit is not None: return strSplit.join([])
62         else: return []
63
64 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
65  
66 class XMLBuilderMixin(object):
67     """Abstract base class that guides the building of python objects from XML. 
68     Depends upon the interface defined by XMLBuilderObjectBase."""
69
70     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
71     #~ Constants / Variables / Etc.
72     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
73
74     _seperator = '.'
75     NamespaceSynonyms = {}
76
77     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
78     #~ Special
79     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
80
81     def __init__(self):
82         self._elements = []
83         self._current_namespacemap = XMLNamespaceMap()
84
85     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
86     #~ Protected Methods
87     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
88
89     def _GetElementFactory(self, owner, element, node, attributes, namespacemap):
90         """Allows a derived class to return an instance factory (a class, since python is so awesome) for
91         this particular data.  The template method makes no restrictions, except that the instance factory
92         must be able to accept the same arguments as this method."""
93         raise ElementFactoryError('No Class Registered for %r' % (node,))
94
95     def _SaveStackState(self, bFull=0):
96         """Returns a stack state token that can be given to _RestoreStackState"""
97         if bFull:
98             return self._elements[:]
99         else:
100             return len(self._elements)
101
102     def _RestoreStackState(self, statetoken):
103         """Uses the stack state token returned by _SaveStackState to restore the
104         existing element stack to a prior level."""
105         if isinstance(statetoken, int):
106             self._elements[statetoken:] = []
107         else:
108             self._elements = statetoken
109
110     def _GetOwner(self):
111         """Used by _start_element to point to the owner of the node.  Derived classes should
112         override this method the owner is different than the XMLBuilder"""
113         return self
114
115     def _GetElement(self, idx=-1):
116         """Used by _start_element to point to the parent of the node.  Derived classes should
117         override this method the owner is different than the XMLBuilder"""
118         if self._elements:
119             element = self._elements[idx]
120             try: xmlGetResult = element._xmlGetElement
121             except AttributeError: return element
122             else: return xmlGetResult()
123         else: return None
124
125     def _GetAttributes(self, node, attributes):
126         result = {}
127         for attrname, attrvalue in attributes.iteritems():
128             attrnamespace, attrname = self._SplitQualifiedName(attrname)
129             if not attrnamespace or attrnamespace == node[0]:
130                 result[node[0], attrname] = attrvalue
131                 result[attrname] = attrvalue
132             else: result[attrnamespace, attrname] = attrvalue
133         return result
134
135     def _start_namespace_decl_handler(self, prefix, uri):
136         """Part of the tree-style template method, called at the before the beginning of an XML node parse
137         to manage namespaces."""
138         # Don't forget to substitute our namespace synonyms!
139         uri = self.NamespaceSynonyms.get(uri or None, uri)
140         # Add the prefix/uri to our current namespace mapping
141         self._current_namespacemap.setxmlns(prefix, uri)
142
143     def _end_namespace_decl_handler(self, prefix):
144         """Part of the tree-style template method, called at the after the end of an XML node parse
145         to manage namespaces."""
146         pass
147
148     def _start_element(self, name, attributes):
149         """Part of the tree-style template method, called at the beginning of an XML node parse.
150         Instantiates the element returned by _GetElementFactory."""
151         node = self._SplitQualifiedName(name)
152         newattributes = self._GetAttributes(node, attributes)
153         args = (self._GetOwner(), self._GetElement(), node, newattributes, self._current_namespacemap)
154         self._current_namespacemap  = self._current_namespacemap.newchain()
155
156         build_factory = self._GetElementFactory(*args)
157
158         if isinstance(build_factory, tuple):
159             newelement = Curry.callTuple(build_factory)
160         else:
161             newelement = build_factory(*args)
162
163         if self._elements:
164             self._elements[-1]._addElement(node, newelement._xmlGetElement())
165         self._elements.append(newelement)
166
167         self._elements[-1]._xmlInitStarted()
168         return self._elements[-1]
169
170     def _end_element(self, name):
171         """Part of the tree-style template method, called at the closing of an XML node parse.
172         Simply notifies the element that it is complete."""
173
174         if self._elements:
175             try: _xmlInitFinalized = self._elements[-1]._xmlInitFinalized
176             except AttributeError: pass
177             else:
178                 _xmlInitFinalized()
179
180             element = self._elements.pop()
181             self._current_namespacemap = self._current_namespacemap.nextmap
182
183             element._xmlInitComplete()
184
185             # Get the appropriate result, if it is overriden
186             try: xmlGetElement = element._xmlGetElement
187             except AttributeError: result = element
188             else:
189                 result = xmlGetElement()
190         else:
191             result = None
192             self._current_namespacemap = XMLNamespaceMap()
193         return result
194
195     def _char_data(self, data):
196         """Part of the tree-style template method, called when PCData is found."""
197         self._elements[-1]._addData(data)
198
199     def _SplitQualifiedName(self, combined):
200         idx = combined.rfind(self._seperator)
201         if idx < 0:
202             namespace = None
203             name = combined
204         else:
205             namespace = combined[0:idx]
206             name = combined[idx + len(self._seperator):]
207         namespace = self.NamespaceSynonyms.get(namespace or None, namespace)
208         return namespace, name
209
210 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
211
212 class XMLExpatBuilderMixin(object):
213     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
214     #~ Constants / Variables / Etc.
215     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
216
217     _encoding = 'ASCII'
218
219     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
220     #~ Public Methods
221     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
222
223     def Parse(self, *args, **kw):
224         """Starts the building of python objects using the XML parser.  Assumes first argument is string-like object."""
225         self._elements, self._LastCompleteElement  = [], None
226         parser = self._CreateParser()
227
228         self._do_parse(parser.Parse, *args, **kw)
229
230         result, self._LastCompleteElement = self._LastCompleteElement, None
231         return result
232
233     def ParseFile(self, *args, **kw):
234         """Starts the building of python objects using the XML parser.  Assumes first argument is a file-like object."""
235         self._elements, self._LastCompleteElement  = [], None
236         parser = self._CreateParser()
237
238         self._do_parse(parser.ParseFile, *args, **kw)
239
240         result, self._LastCompleteElement = self._LastCompleteElement, None
241         return result
242
243     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
244     #~ Protected Methods
245     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
246
247     def _CreateParser(self):
248         """Creates the Expat parser in a python-OO way."""
249         parser = _ExpatParserCreate(self._encoding, self._seperator)
250         try:
251             #if using python 2.3 or above, the buffer text should speed things up
252             parser.buffer_text = True
253         except AttributeError:
254             # Otherwise, ignore the set attribute error
255             pass
256
257         parser.returns_unicode = self._encoding != 'ASCII' and 1 or 0
258         parser.StartElementHandler = self._start_element
259         parser.EndElementHandler = self._end_element_ex
260         parser.CharacterDataHandler = self._char_data
261         parser.StartNamespaceDeclHandler = self._start_namespace_decl_handler
262         parser.EndNamespaceDeclHandler = self._end_namespace_decl_handler
263         return parser
264
265     def _end_element_ex(self, name):
266         """Manages the _LastCompleteElement variable."""
267         self._LastCompleteElement = self._end_element(name)
268
269     def _do_parse(self, call, *args, **kw):
270         try:
271             call(*args, **kw)
272         except StandardError, err:
273             setattr(err, 'xmlsource', getattr(err, 'xmlsource', args[0]))
274             raise
275
276 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
277
278 class XMLBuilder(XMLBuilderMixin, XMLExpatBuilderMixin):
279     """Abstract base class closer to actualizing python object building. 
280     See XMLObjectify, or XMLClassBuilder for more concrete builders."""
281
282     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
283     #~ Special
284     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
285
286     def __call__(self, *args, **kw):
287         """Calls ParseFile if the first argument is an open file, and Parse otherwise."""
288         if isinstance(args[0], file):
289             return self.ParseFile(*args, **kw)
290         else: return self.Parse(*args, **kw)
291
Note: See TracBrowser for help on using the browser.