Setting tag sface-0.1-20
[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
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         exportButton = QPushButton("&Export Call")
162         layoutButtons = QHBoxLayout()
163         layoutButtons.addWidget(exportButton)
164         layoutButtons.addStretch()
165
166         self.layout = QVBoxLayout()
167         self.layout.addWidget(labelCalls)
168         self.layout.addWidget(self.callTable)
169         self.layout.addWidget(labelArgs)
170         self.layout.addWidget(self.argsTable)
171         self.layout.addWidget(labelXml)
172         self.layout.addWidget(self.xmlView)
173         self.layout.addLayout(layoutButtons)
174
175         self.setLayout(self.layout)
176
177         self.connect(self.callTable, SIGNAL("itemSelectionChanged ()"), self.onCallSelect)
178         self.connect(exportButton, SIGNAL("clicked()"), self.onExportClicked)
179
180     def getAndPrint(self, rawOutput):
181         self.reader.store(rawOutput)
182         self.reader.extractXml()
183         self.updateCallTable()
184
185     def updateCallTable(self):
186         self.callTable.clear()
187         self.callTable.setColumnCount(3)
188         self.callTable.setHorizontalHeaderLabels(["name", "status", "faultString"])
189         self.callTable.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
190
191         calls = self.reader.calls
192         self.callTable.setRowCount(len(calls))
193
194         row = 0
195         reverse_calls = list(enumerate(calls))
196         reverse_calls.reverse()
197         for (index,call) in reverse_calls:
198             item = QTableWidgetItem(call.get("methodName", "unknown"))
199             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
200             item.setData(Qt.UserRole, index)
201             self.callTable.setItem(row, 0, item)
202
203             item = QTableWidgetItem(call.get("kind", "unknown"))
204             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
205             item.setData(Qt.UserRole, index)
206             self.callTable.setItem(row, 1, item)
207
208             item = QTableWidgetItem(call.get("faultString", ""))
209             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
210             item.setData(Qt.UserRole, index)
211             self.callTable.setItem(row, 2, item)
212
213             row = row + 1
214
215         self.callTable.resizeColumnsToContents()
216
217     def updateArgsTable(self, args):
218         self.argsTable.clear()
219         self.argsTable.setColumnCount(1)
220         self.argsTable.setHorizontalHeaderLabels(["param"])
221         self.argsTable.horizontalHeader().hide()
222         self.argsTable.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
223
224         self.argsTable.setRowCount(len(args))
225
226         row = 0
227         for arg in args:
228             if len(arg)>255:
229                 arg = arg[:255] + "..."
230
231             item = QTableWidgetItem(arg)
232             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
233             self.argsTable.setItem(row, 0, item)
234
235             row = row + 1
236
237         self.argsTable.resizeColumnsToContents()
238
239     def getSelectedCall(self):
240         selItems = self.callTable.selectedItems()
241         if (len(selItems) <= 0):
242             return None
243
244         row = selItems[0].data(Qt.UserRole).toInt()[0]
245
246         calls = self.reader.calls
247         if len(calls)<=row:
248             return None
249
250         call = calls[row]
251
252         return call
253
254     def onCallSelect(self):
255         call = self.getSelectedCall()
256
257         if not call:
258             return
259
260         xml = "<debug>" + call.get("request","") + call.get("reply", "") + "</debug>"
261         #xml = call.get("request","") + call.get("reply", "")
262
263         self.displayXml(xml)
264
265         self.updateArgsTable(call.get("args",[]))
266
267     def onExportClicked(self):
268         call = self.getSelectedCall()
269
270         if not call:
271             return
272
273         filename = QFileDialog.getSaveFileName(self, 'Save File', '.')
274
275         f = open(filename, "w")
276         f.write(call.get("request","") + "\n")
277         f.write(call.get("reply","") + "\n")
278         f.close()
279
280
281     def displayXml(self, xml):
282         self.document = QDomDocument("XMLRPC Tracker")
283         self.document.setContent(xml)
284         self.model = DomModel(self.document, self)
285         self.xmlView.setModel(self.model)
286
287
288 # There's one global tracker for XMLRPCs. Use init_tracker() to create it, and
289 # get_tracker() to get it.
290
291 glo_tracker = None
292
293 def init_tracker(parent):
294     global glo_tracker
295
296     glo_tracker = XmlrpcTracker(parent)
297
298     return glo_tracker
299
300 def get_tracker():
301     global glo_tracker
302
303     return glo_tracker
304
305