use xmlrpclib.loads rather than parsing manually
[sface.git] / sface / xmlrpcwindow.py
1 import re
2 import xmlrpclib
3 from lxml import etree
4 from PyQt4.QtXml import QDomDocument
5 from sface.xmlwidget import XmlWindow, DomModel, XmlView, XmlDelegate, ElemNodeDelegate, TextNodeDelegate, CommentNodeDelegate
6
7 from PyQt4.QtCore import *
8 from PyQt4.QtGui import *
9
10 class XmlrpcReader():
11     def __init__(self):
12         self.rawOutput = None
13         self.responses = []
14         self.calls = []
15
16     def store(self, rawOutput):
17         self.rawOutput = rawOutput
18
19     def parseMethodCall(self, mc):
20         mc = str(mc)
21
22         request = {}
23
24         try:
25             tree = etree.fromstring(mc)
26         except etree.XMLSyntaxError, e:
27             return request
28
29         if tree.tag != "methodCall" or (len(list(tree))==0):
30             return request
31
32         try:
33             (pythonParams, methodName) = xmlrpclib.loads(etree.tostring(tree))
34         except:  # in case something went wrong when unmarashaling...
35             pythonParams = []
36             methodName = "<exception in parseMethodCall>"
37
38         request["args"] = [str(x) for x in pythonParams]
39         request["methodName"] = methodName # methodNames[0].text
40
41         return request
42
43     def parseMethodResponse(self, mr):
44         mr = str(mr) # PyQT supplies a QByteArray; make it a string
45
46         response = {"kind": "unknown"}
47
48         try:
49             tree = etree.fromstring(mr)
50         except etree.XMLSyntaxError, e:
51             print "failed to parse XML response", str(e)
52             #file("badparse.xml","w").write(mr)
53             return response
54
55         if tree.tag != "methodResponse" or (len(list(tree))==0):
56             return response
57
58         # a fault should look like:
59         #     kind: "fault"
60         #     faultCode: "102"
61         #     faultString: "Register: Missing authority..."
62
63         faults = tree.xpath("//methodResponse/fault")
64         for fault in faults:
65             response["kind"] = "fault"
66             structs = fault.xpath("value/struct")
67             for struct in structs:
68                 members = struct.xpath("member")
69                 for member in members:
70                     names = member.xpath("name")
71                     values = member.xpath("value")
72                     if (names) and (values):
73                         name = names[0]
74                         value = values[0]
75                         if len(list(value))>0:
76                             data = list(value)[0]
77                             response[name.text] = data.text
78             # once we have the first fault, return
79             return response
80
81         # whatever didn't fault must have succeeded?
82         # "success" really isn't quite the right word. The best we can say is
83         # that the call was executed. The actual op may or may not have
84         # succeeded.
85         response["kind"] = "executed"
86
87         return response
88
89     def extractXml(self):
90         pttrnAsk = '<methodCall>.*?</methodCall>'
91         pttrnAns = '<methodResponse>.*?</methodResponse>'
92         requests = re.compile(pttrnAsk, re.DOTALL).findall(self.rawOutput)
93         replies = re.compile(pttrnAns, re.DOTALL).findall(self.rawOutput)
94         # cleaning
95         requests = [ x.replace('\\n','\n') for x in requests ]
96         replies = [ x.replace('\\n','\n').replace("'\nbody: '", '').replace("\"\nbody: '", '') for x in replies ]
97         # A well-formed XML document must have one, and only one, top-level element
98
99         self.responses = []
100
101         self.xml = ""
102         for requestXml in requests:
103             self.xml += requestXml
104             callDict = {"request": requestXml}
105             # we could have less responses than calls, so guard the pop
106             if replies:
107                 replyXml = replies.pop(0)
108                 self.xml += replyXml
109
110                 # parse the response to extract fault information
111                 responseDict = self.parseMethodResponse(replyXml)
112                 self.responses.append(responseDict)
113
114                 requestDict = self.parseMethodCall(requestXml)
115
116                 callDict["reply"] = replyXml
117                 callDict.update(responseDict)
118                 callDict.update(requestDict)
119
120             self.calls.append(callDict)
121
122         # just in case somehow we ended up with more responses than calls
123         while replies:
124             replyXml = replies.pop()
125             self.xml += replyXml
126             self.responses.append(self.parseMethodResponse(replyXml))
127
128         return self.xml
129
130     def stats(self):
131         # statistics: round-trip time, size of the com
132         pass
133
134 class XmlrpcTracker(QWidget):
135     def __init__(self, parent=None):
136         QWidget.__init__(self, parent=parent)
137
138         self.reader = XmlrpcReader()
139
140         labelCalls = QLabel("Calls: (from most recent to least recent)")
141
142         self.callTable = QTableWidget(self)
143         self.callTable.setSelectionBehavior(QAbstractItemView.SelectRows)
144         self.callTable.setSelectionMode(QAbstractItemView.SingleSelection)
145         self.callTable.setMaximumHeight(140)
146
147         labelArgs = QLabel("Params:")
148
149         self.argsTable = QTableWidget(self)
150         self.argsTable.setMaximumHeight(140)
151
152         labelXml = QLabel("XMLRPC Contents")
153         self.xmlView = XmlView(self)
154
155         self.delegate = XmlDelegate(self)
156         self.delegate.insertNodeDelegate('element', ElemNodeDelegate())
157         self.delegate.insertNodeDelegate('text', TextNodeDelegate())
158         self.delegate.insertNodeDelegate('comment', CommentNodeDelegate())
159         self.xmlView.setItemDelegate(self.delegate)
160
161         self.layout = QVBoxLayout()
162         self.layout.addWidget(labelCalls)
163         self.layout.addWidget(self.callTable)
164         self.layout.addWidget(labelArgs)
165         self.layout.addWidget(self.argsTable)
166         self.layout.addWidget(labelXml)
167         self.layout.addWidget(self.xmlView)
168
169         self.setLayout(self.layout)
170
171         self.connect(self.callTable, SIGNAL("itemSelectionChanged ()"), self.onCallSelect)
172
173     def getAndPrint(self, rawOutput):
174         self.reader.store(rawOutput)
175         self.reader.extractXml()
176         self.updateCallTable()
177
178     def updateCallTable(self):
179         self.callTable.clear()
180         self.callTable.setColumnCount(2)
181         self.callTable.setHorizontalHeaderLabels(["name", "status"])
182
183         calls = self.reader.calls
184         self.callTable.setRowCount(len(calls))
185
186         row = 0
187         reverse_calls = list(enumerate(calls))
188         reverse_calls.reverse()
189         for (index,call) in reverse_calls:
190             item = QTableWidgetItem(call.get("methodName", "unknown"))
191             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
192             item.setData(Qt.UserRole, index)
193             self.callTable.setItem(row, 0, item)
194
195             item = QTableWidgetItem(call.get("kind", "unknown"))
196             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
197             item.setData(Qt.UserRole, index)
198             self.callTable.setItem(row, 1, item)
199
200             row = row + 1
201
202         self.callTable.resizeColumnsToContents()
203
204     def updateArgsTable(self, args):
205         self.argsTable.clear()
206         self.argsTable.setColumnCount(1)
207         self.argsTable.setHorizontalHeaderLabels(["param"])
208         self.argsTable.horizontalHeader().hide()
209         self.argsTable.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
210
211         self.argsTable.setRowCount(len(args))
212
213         row = 0
214         for arg in args:
215             if len(arg)>255:
216                 arg = arg[:255] + "..."
217
218             item = QTableWidgetItem(arg)
219             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
220             self.argsTable.setItem(row, 0, item)
221
222             row = row + 1
223
224         self.argsTable.resizeColumnsToContents()
225
226     def onCallSelect(self):
227         selItems = self.callTable.selectedItems()
228         if (len(selItems) <= 0):
229             return
230
231         row = selItems[0].data(Qt.UserRole).toInt()[0]
232
233         calls = self.reader.calls
234         if len(calls)<=row:
235             return
236
237         call = calls[row]
238
239         xml = "<debug>" + call.get("request","") + call.get("reply", "") + "</debug>"
240         #xml = call.get("request","") + call.get("reply", "")
241
242         self.displayXml(xml)
243
244         self.updateArgsTable(call.get("args",[]))
245
246     def displayXml(self, xml):
247         self.document = QDomDocument("XMLRPC Tracker")
248         self.document.setContent(xml)
249         self.model = DomModel(self.document, self)
250         self.xmlView.setModel(self.model)
251
252
253 # There's one global tracker for XMLRPCs. Use init_tracker() to create it, and
254 # get_tracker() to get it.
255
256 glo_tracker = None
257
258 def init_tracker(parent):
259     global glo_tracker
260
261     glo_tracker = XmlrpcTracker(parent)
262
263     return glo_tracker
264
265 def get_tracker():
266     global glo_tracker
267
268     return glo_tracker
269
270