1 # Copyright (c) Twisted Matrix Laboratories.
2 # See LICENSE for details.
6 Dict client protocol implementation.
8 @author: Pavel Pergamenshchik
11 from twisted.protocols import basic
12 from twisted.internet import defer, protocol
13 from twisted.python import log
14 from StringIO import StringIO
17 """Chew one dqstring or atom from beginning of line and return (param, remaningline)"""
20 elif line[0] != '"': # atom
26 if mode == 2: # skip the opening quote
32 io.read(1) # skip the separating space
33 return (res, io.read())
37 return (None, line) # unexpected end of string
40 return (res, io.read())
42 return (None, line) # unexpected end of string
45 return (res, io.read())
49 """Munch a string into an 'atom'"""
50 # FIXME: proper quoting
51 return filter(lambda x: not (x in map(chr, range(33)+[34, 39, 92])), line)
54 mustquote = range(33)+[34, 39, 92]
57 if ord(c) in mustquote:
64 if len(line) == 1 and line == '.':
67 if len(line) > 1 and line[0:2] == '..':
72 """A word definition"""
73 def __init__(self, name, db, dbdesc, text):
77 self.text = text # list of strings not terminated by newline
79 class DictClient(basic.LineReceiver):
80 """dict (RFC2229) client"""
82 data = None # multiline data
93 def connectionMade(self):
97 def sendLine(self, line):
98 """Throw up if the line is longer than 1022 characters"""
99 if len(line) > self.MAX_LENGTH - 2:
100 raise ValueError("DictClient tried to send a too long line")
101 basic.LineReceiver.sendLine(self, line)
103 def lineReceived(self, line):
105 line = line.decode("UTF-8")
106 except UnicodeError: # garbage received, skip
108 if self.mode == "text": # we are receiving textual data
112 log.msg("DictClient got invalid line from server -- %s" % line)
113 self.protocolError("Invalid line from server")
114 self.transport.LoseConnection()
118 method = getattr(self, 'dictCode_%s_%s' % (code, self.state), self.dictCode_default)
121 def dictCode_default(self, line):
123 log.msg("DictClient got unexpected message from server -- %s" % line)
124 self.protocolError("Unexpected server message")
125 self.transport.loseConnection()
127 def dictCode_221_ready(self, line):
128 """We are about to get kicked off, do nothing"""
131 def dictCode_220_conn(self, line):
132 """Greeting message"""
136 def dictCode_530_conn(self):
137 self.protocolError("Access denied")
138 self.transport.loseConnection()
140 def dictCode_420_conn(self):
141 self.protocolError("Server temporarily unavailable")
142 self.transport.loseConnection()
144 def dictCode_421_conn(self):
145 self.protocolError("Server shutting down at operator request")
146 self.transport.loseConnection()
148 def sendDefine(self, database, word):
149 """Send a dict DEFINE command"""
150 assert self.state == "ready", "DictClient.sendDefine called when not in ready state"
151 self.result = None # these two are just in case. In "ready" state, result and data
152 self.data = None # should be None
153 self.state = "define"
154 command = "DEFINE %s %s" % (makeAtom(database.encode("UTF-8")), makeWord(word.encode("UTF-8")))
155 self.sendLine(command)
157 def sendMatch(self, database, strategy, word):
158 """Send a dict MATCH command"""
159 assert self.state == "ready", "DictClient.sendMatch called when not in ready state"
163 command = "MATCH %s %s %s" % (makeAtom(database), makeAtom(strategy), makeAtom(word))
164 self.sendLine(command.encode("UTF-8"))
166 def dictCode_550_define(self, line):
167 """Invalid database"""
169 self.defineFailed("Invalid database")
171 def dictCode_550_match(self, line):
172 """Invalid database"""
174 self.matchFailed("Invalid database")
176 def dictCode_551_match(self, line):
177 """Invalid strategy"""
179 self.matchFailed("Invalid strategy")
181 def dictCode_552_define(self, line):
184 self.defineFailed("No match")
186 def dictCode_552_match(self, line):
189 self.matchFailed("No match")
191 def dictCode_150_define(self, line):
192 """n definitions retrieved"""
195 def dictCode_151_define(self, line):
196 """Definition text follows"""
198 (word, line) = parseParam(line)
199 (db, line) = parseParam(line)
200 (dbdesc, line) = parseParam(line)
201 if not (word and db and dbdesc):
202 self.protocolError("Invalid server response")
203 self.transport.loseConnection()
205 self.result.append(Definition(word, db, dbdesc, []))
208 def dictCode_152_match(self, line):
209 """n matches found, text follows"""
214 def dictCode_text_define(self, line):
215 """A line of definition text received"""
216 res = parseText(line)
218 self.mode = "command"
219 self.result[-1].text = self.data
222 self.data.append(line)
224 def dictCode_text_match(self, line):
225 """One line of match text received"""
227 p1, t = parseParam(s)
228 p2, t = parseParam(t)
230 res = parseText(line)
232 self.mode = "command"
233 self.result = map(l, self.data)
236 self.data.append(line)
238 def dictCode_250_define(self, line):
245 def dictCode_250_match(self, line):
252 def protocolError(self, reason):
253 """override to catch unexpected dict protocol conditions"""
256 def dictConnected(self):
257 """override to be notified when the server is ready to accept commands"""
260 def defineFailed(self, reason):
261 """override to catch reasonable failure responses to DEFINE"""
264 def defineDone(self, result):
265 """override to catch succesful DEFINE"""
268 def matchFailed(self, reason):
269 """override to catch resonable failure responses to MATCH"""
272 def matchDone(self, result):
273 """override to catch succesful MATCH"""
277 class InvalidResponse(Exception):
281 class DictLookup(DictClient):
282 """Utility class for a single dict transaction. To be used with DictLookupFactory"""
284 def protocolError(self, reason):
285 if not self.factory.done:
286 self.factory.d.errback(InvalidResponse(reason))
287 self.factory.clientDone()
289 def dictConnected(self):
290 if self.factory.queryType == "define":
291 apply(self.sendDefine, self.factory.param)
292 elif self.factory.queryType == "match":
293 apply(self.sendMatch, self.factory.param)
295 def defineFailed(self, reason):
296 self.factory.d.callback([])
297 self.factory.clientDone()
298 self.transport.loseConnection()
300 def defineDone(self, result):
301 self.factory.d.callback(result)
302 self.factory.clientDone()
303 self.transport.loseConnection()
305 def matchFailed(self, reason):
306 self.factory.d.callback([])
307 self.factory.clientDone()
308 self.transport.loseConnection()
310 def matchDone(self, result):
311 self.factory.d.callback(result)
312 self.factory.clientDone()
313 self.transport.loseConnection()
316 class DictLookupFactory(protocol.ClientFactory):
317 """Utility factory for a single dict transaction"""
318 protocol = DictLookup
321 def __init__(self, queryType, param, d):
322 self.queryType = queryType
327 def clientDone(self):
328 """Called by client when done."""
332 def clientConnectionFailed(self, connector, error):
333 self.d.errback(error)
335 def clientConnectionLost(self, connector, error):
337 self.d.errback(error)
339 def buildProtocol(self, addr):
345 def define(host, port, database, word):
346 """Look up a word using a dict server"""
348 factory = DictLookupFactory("define", (database, word), d)
350 from twisted.internet import reactor
351 reactor.connectTCP(host, port, factory)
354 def match(host, port, database, strategy, word):
355 """Match a word using a dict server"""
357 factory = DictLookupFactory("match", (database, strategy, word), d)
359 from twisted.internet import reactor
360 reactor.connectTCP(host, port, factory)