Fix bug
[plcapi.git] / PLC / Xml.py
1 #!/usr/bin/python 
2 from types import StringTypes
3 from lxml import etree
4 from StringIO import StringIO
5
6 # helper functions to help build xpaths
7 class XpathFilter:
8     @staticmethod
9
10     def filter_value(key, value):
11         xpath = ""    
12         if isinstance(value, str):
13             if '*' in value:
14                 value = value.replace('*', '')
15                 xpath = 'contains(%s, "%s")' % (key, value)
16             else:
17                 xpath = '%s="%s"' % (key, value)                
18         return xpath
19
20     @staticmethod
21     def xpath(filter={}):
22         xpath = ""
23         if filter:
24             filter_list = []
25             for (key, value) in filter.items():
26                 if key == 'text':
27                     key = 'text()'
28                 else:
29                     key = '@'+key
30                 if isinstance(value, str):
31                     filter_list.append(XpathFilter.filter_value(key, value))
32                 elif isinstance(value, list):
33                     stmt = ' or '.join([XpathFilter.filter_value(key, str(val)) for val in value])
34                     filter_list.append(stmt)   
35             if filter_list:
36                 xpath = ' and '.join(filter_list)
37                 xpath = '[' + xpath + ']'
38         return xpath
39
40 # a wrapper class around lxml.etree._Element
41 # the reason why we need this one is because of the limitations
42 # we've found in xpath to address documents with multiple namespaces defined
43 # in a nutshell, we deal with xml documents that have
44 # a default namespace defined (xmlns="http://default.com/") and specific prefixes defined
45 # (xmlns:foo="http://foo.com")
46 # according to the documentation instead of writing
47 # element.xpath ( "//node/foo:subnode" ) 
48 # we'd then need to write xpaths like
49 # element.xpath ( "//{http://default.com/}node/{http://foo.com}subnode" ) 
50 # which is a real pain..
51 # So just so we can keep some reasonable programming style we need to manage the
52 # namespace map that goes with the _Element (its internal .nsmap being unmutable)
53
54 class XmlElement:
55     def __init__(self, element, namespaces):
56         self.element = element
57         self.namespaces = namespaces
58         
59     # redefine as few methods as possible
60     def xpath(self, xpath, namespaces=None):
61         if not namespaces:
62             namespaces = self.namespaces 
63         elems = self.element.xpath(xpath, namespaces=namespaces)
64         return [XmlElement(elem, namespaces) for elem in elems]
65
66     def add_element(self, tagname, **kwds):
67         element = etree.SubElement(self.element, tagname, **kwds)
68         return XmlElement(element, self.namespaces)
69
70     def append(self, elem):
71         if isinstance(elem, XmlElement):
72             self.element.append(elem.element)
73         else:
74             self.element.append(elem)
75
76     def getparent(self):
77         return XmlElement(self.element.getparent(), self.namespaces)
78
79     def get_instance(self, instance_class=None, fields=[]):
80         """
81         Returns an instance (dict) of this xml element. The instance
82         holds a reference to this xml element.   
83         """
84         if not instance_class:
85             instance_class = Object 
86         if not fields and hasattr(instance_class, 'fields'):
87             fields = instance_class.fields
88
89         if not fields:
90             instance = instance_class(self.attrib, self)
91         else:
92             instance = instance_class({}, self)
93             for field in fields:
94                 if field in self.attrib:
95                    instance[field] = self.attrib[field]  
96         return instance             
97
98     def add_instance(self, name, instance, fields=[]):
99         """
100         Adds the specifed instance(s) as a child element of this xml 
101         element. 
102         """
103         if not fields and hasattr(instance, 'keys'):
104             fields = instance.keys()
105         elem = self.add_element(name)
106         for field in fields:
107             if field in instance and instance[field]:
108                 elem.set(field, unicode(instance[field]))
109         return elem                  
110
111     def remove_elements(self, name):
112         """
113         Removes all occurences of an element from the tree. Start at
114         specified root_node if specified, otherwise start at tree's root.
115         """
116         
117         if not element_name.startswith('//'):
118             element_name = '//' + element_name
119         elements = self.element.xpath('%s ' % name, namespaces=self.namespaces) 
120         for element in elements:
121             parent = element.getparent()
122             parent.remove(element)
123
124     def delete(self):
125         parent = self.getparent()
126         parent.remove(self)
127
128     def remove(self, element):
129         if isinstance(element, XmlElement):
130             self.element.remove(element.element)
131         else:
132             self.element.remove(element)
133
134     def set_text(self, text):
135         self.element.text = text
136     
137     # Element does not have unset ?!?
138     def unset(self, key):
139         del self.element.attrib[key]
140   
141     def toxml(self):
142         return etree.tostring(self.element, encoding='UTF-8', pretty_print=True)                    
143
144     def __str__(self):
145         return self.toxml()
146
147     ### other method calls or attribute access like .text or .tag or .get 
148     # are redirected on self.element
149     def __getattr__ (self, name):
150         if not hasattr(self.element, name):
151             raise AttributeError, name
152         return getattr(self.element, name)
153
154 class Xml:
155  
156     def __init__(self, xml=None, namespaces=None):
157         self.root = None
158         self.namespaces = namespaces
159         self.default_namespace = None
160         self.schema = None
161         if isinstance(xml, basestring):
162             self.parse_xml(xml)
163         if isinstance(xml, XmlElement):
164             self.root = xml
165             self.namespaces = xml.namespaces
166         elif isinstance(xml, etree._ElementTree) or isinstance(xml, etree._Element):
167             self.parse_xml(etree.tostring(xml))
168
169     def parse_xml(self, xml):
170         """
171         parse rspec into etree
172         """
173         parser = etree.XMLParser(remove_blank_text=True)
174         try:
175             tree = etree.parse(xml, parser)
176         except IOError:
177             # 'rspec' file doesnt exist. 'rspec' is proably an xml string
178             try:
179                 tree = etree.parse(StringIO(xml), parser)
180             except Exception, e:
181                 raise Exception, str(e)
182         root = tree.getroot()
183         self.namespaces = dict(root.nsmap)
184         # set namespaces map
185         if 'default' not in self.namespaces and None in self.namespaces: 
186             # If the 'None' exist, then it's pointing to the default namespace. This makes 
187             # it hard for us to write xpath queries for the default naemspace because lxml 
188             # wont understand a None prefix. We will just associate the default namespeace 
189             # with a key named 'default'.     
190             self.namespaces['default'] = self.namespaces.pop(None)
191             
192         else:
193             self.namespaces['default'] = 'default' 
194
195         self.root = XmlElement(root, self.namespaces)
196         # set schema
197         for key in self.root.attrib.keys():
198             if key.endswith('schemaLocation'):
199                 # schemaLocation should be at the end of the list.
200                 # Use list comprehension to filter out empty strings 
201                 schema_parts  = [x for x in self.root.attrib[key].split(' ') if x]
202                 self.schema = schema_parts[1]    
203                 namespace, schema  = schema_parts[0], schema_parts[1]
204                 break
205
206     def parse_dict(self, d, root_tag_name='xml', element = None):
207         if element is None: 
208             if self.root is None:
209                 self.parse_xml('<%s/>' % root_tag_name)
210             element = self.root.element
211
212         if 'text' in d:
213             text = d.pop('text')
214             element.text = text
215
216         # handle repeating fields
217         for (key, value) in d.items():
218             if isinstance(value, list):
219                 value = d.pop(key)
220                 for val in value:
221                     if isinstance(val, dict):
222                         child_element = etree.SubElement(element, key)
223                         self.parse_dict(val, key, child_element)
224                     elif isinstance(val, basestring):
225                         child_element = etree.SubElement(element, key).text = val
226
227             elif isinstance(value, int):
228                 d[key] = unicode(d[key])
229             elif value is None:
230                 d.pop(key)
231
232         # element.attrib.update will explode if DateTimes are in the
233         # dcitionary.
234         d=d.copy()
235         # looks like iteritems won't stand side-effects
236         for k in d.keys():
237             if not isinstance(d[k],StringTypes):
238                 del d[k]
239
240         element.attrib.update(d)
241
242     def validate(self, schema):
243         """
244         Validate against rng schema
245         """
246         relaxng_doc = etree.parse(schema)
247         relaxng = etree.RelaxNG(relaxng_doc)
248         if not relaxng(self.root):
249             error = relaxng.error_log.last_error
250             message = "%s (line %s)" % (error.message, error.line)
251             raise Exception, message
252         return True
253
254     def xpath(self, xpath, namespaces=None):
255         if not namespaces:
256             namespaces = self.namespaces
257         return self.root.xpath(xpath, namespaces=namespaces)
258
259     def set(self, key, value):
260         return self.root.set(key, value)
261
262     def remove_attribute(self, name, element=None):
263         if not element:
264             element = self.root
265         element.remove_attribute(name) 
266
267     def add_element(self, *args, **kwds):
268         """
269         Wrapper around etree.SubElement(). Adds an element to 
270         specified parent node. Adds element to root node is parent is 
271         not specified. 
272         """
273         return self.root.add_element(*args, **kwds)
274
275     def remove_elements(self, name, element = None):
276         """
277         Removes all occurences of an element from the tree. Start at 
278         specified root_node if specified, otherwise start at tree's root.   
279         """
280         if not element:
281             element = self.root
282
283         element.remove_elements(name)
284
285     def add_instance(self, *args, **kwds):
286         return self.root.add_instance(*args, **kwds)
287
288     def get_instance(self, *args, **kwds):
289         return self.root.get_instnace(*args, **kwds)
290
291     def get_element_attributes(self, elem=None, depth=0):
292         if elem == None:
293             elem = self.root
294         if not hasattr(elem, 'attrib'):
295             # this is probably not an element node with attribute. could be just and an
296             # attribute, return it
297             return elem
298         attrs = dict(elem.attrib)
299         attrs['text'] = str(elem.text).strip()
300         attrs['parent'] = elem.getparent()
301         if isinstance(depth, int) and depth > 0:
302             for child_elem in list(elem):
303                 key = str(child_elem.tag)
304                 if key not in attrs:
305                     attrs[key] = [self.get_element_attributes(child_elem, depth-1)]
306                 else:
307                     attrs[key].append(self.get_element_attributes(child_elem, depth-1))
308         else:
309             attrs['child_nodes'] = list(elem)
310         return attrs
311
312     def append(self, elem):
313         return self.root.append(elem)
314
315     def iterchildren(self):
316         return self.root.iterchildren()    
317
318     def merge(self, in_xml):
319         pass
320
321     def __str__(self):
322         return self.toxml()
323
324     def toxml(self):
325         return etree.tostring(self.root.element, encoding='UTF-8', pretty_print=True)  
326     
327     # XXX smbaker, for record.load_from_string
328     def todict(self, elem=None):
329         if elem is None:
330             elem = self.root
331         d = {}
332         d.update(elem.attrib)
333         d['text'] = elem.text
334         for child in elem.iterchildren():
335             if child.tag not in d:
336                 d[child.tag] = []
337             d[child.tag].append(self.todict(child))
338
339         if len(d)==1 and ("text" in d):
340             d = d["text"]
341
342         return d
343         
344     def save(self, filename):
345         f = open(filename, 'w')
346         f.write(self.toxml())
347         f.close()
348
349 # no RSpec in scope 
350 #if __name__ == '__main__':
351 #    rspec = RSpec('/tmp/resources.rspec')
352 #    print rspec
353