| OLD | NEW |
| (Empty) |
| 1 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
| 2 # See LICENSE for details. | |
| 3 | |
| 4 | |
| 5 """ | |
| 6 Dict client protocol implementation. | |
| 7 | |
| 8 @author: U{Pavel Pergamenshchik<mailto:pp64@cornell.edu>} | |
| 9 """ | |
| 10 | |
| 11 from twisted.protocols import basic | |
| 12 from twisted.internet import defer, protocol | |
| 13 from twisted.python import log | |
| 14 from StringIO import StringIO | |
| 15 | |
| 16 def parseParam(line): | |
| 17 """Chew one dqstring or atom from beginning of line and return (param, reman
ingline)""" | |
| 18 if line == '': | |
| 19 return (None, '') | |
| 20 elif line[0] != '"': # atom | |
| 21 mode = 1 | |
| 22 else: # dqstring | |
| 23 mode = 2 | |
| 24 res = "" | |
| 25 io = StringIO(line) | |
| 26 if mode == 2: # skip the opening quote | |
| 27 io.read(1) | |
| 28 while 1: | |
| 29 a = io.read(1) | |
| 30 if a == '"': | |
| 31 if mode == 2: | |
| 32 io.read(1) # skip the separating space | |
| 33 return (res, io.read()) | |
| 34 elif a == '\\': | |
| 35 a = io.read(1) | |
| 36 if a == '': | |
| 37 return (None, line) # unexpected end of string | |
| 38 elif a == '': | |
| 39 if mode == 1: | |
| 40 return (res, io.read()) | |
| 41 else: | |
| 42 return (None, line) # unexpected end of string | |
| 43 elif a == ' ': | |
| 44 if mode == 1: | |
| 45 return (res, io.read()) | |
| 46 res += a | |
| 47 | |
| 48 def makeAtom(line): | |
| 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) | |
| 52 | |
| 53 def makeWord(s): | |
| 54 mustquote = range(33)+[34, 39, 92] | |
| 55 result = [] | |
| 56 for c in s: | |
| 57 if ord(c) in mustquote: | |
| 58 result.append("\\") | |
| 59 result.append(c) | |
| 60 s = "".join(result) | |
| 61 return s | |
| 62 | |
| 63 def parseText(line): | |
| 64 if len(line) == 1 and line == '.': | |
| 65 return None | |
| 66 else: | |
| 67 if len(line) > 1 and line[0:2] == '..': | |
| 68 line = line[1:] | |
| 69 return line | |
| 70 | |
| 71 class Definition: | |
| 72 """A word definition""" | |
| 73 def __init__(self, name, db, dbdesc, text): | |
| 74 self.name = name | |
| 75 self.db = db | |
| 76 self.dbdesc = dbdesc | |
| 77 self.text = text # list of strings not terminated by newline | |
| 78 | |
| 79 class DictClient(basic.LineReceiver): | |
| 80 """dict (RFC2229) client""" | |
| 81 | |
| 82 data = None # multiline data | |
| 83 MAX_LENGTH = 1024 | |
| 84 state = None | |
| 85 mode = None | |
| 86 result = None | |
| 87 factory = None | |
| 88 | |
| 89 def __init__(self): | |
| 90 self.data = None | |
| 91 self.result = None | |
| 92 | |
| 93 def connectionMade(self): | |
| 94 self.state = "conn" | |
| 95 self.mode = "command" | |
| 96 | |
| 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) | |
| 102 | |
| 103 def lineReceived(self, line): | |
| 104 try: | |
| 105 line = line.decode("UTF-8") | |
| 106 except UnicodeError: # garbage received, skip | |
| 107 return | |
| 108 if self.mode == "text": # we are receiving textual data | |
| 109 code = "text" | |
| 110 else: | |
| 111 if len(line) < 4: | |
| 112 log.msg("DictClient got invalid line from server -- %s" % line) | |
| 113 self.protocolError("Invalid line from server") | |
| 114 self.transport.LoseConnection() | |
| 115 return | |
| 116 code = int(line[:3]) | |
| 117 line = line[4:] | |
| 118 method = getattr(self, 'dictCode_%s_%s' % (code, self.state), self.dictC
ode_default) | |
| 119 method(line) | |
| 120 | |
| 121 def dictCode_default(self, line): | |
| 122 """Unkown message""" | |
| 123 log.msg("DictClient got unexpected message from server -- %s" % line) | |
| 124 self.protocolError("Unexpected server message") | |
| 125 self.transport.loseConnection() | |
| 126 | |
| 127 def dictCode_221_ready(self, line): | |
| 128 """We are about to get kicked off, do nothing""" | |
| 129 pass | |
| 130 | |
| 131 def dictCode_220_conn(self, line): | |
| 132 """Greeting message""" | |
| 133 self.state = "ready" | |
| 134 self.dictConnected() | |
| 135 | |
| 136 def dictCode_530_conn(self): | |
| 137 self.protocolError("Access denied") | |
| 138 self.transport.loseConnection() | |
| 139 | |
| 140 def dictCode_420_conn(self): | |
| 141 self.protocolError("Server temporarily unavailable") | |
| 142 self.transport.loseConnection() | |
| 143 | |
| 144 def dictCode_421_conn(self): | |
| 145 self.protocolError("Server shutting down at operator request") | |
| 146 self.transport.loseConnection() | |
| 147 | |
| 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, resu
lt 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) | |
| 156 | |
| 157 def sendMatch(self, database, strategy, word): | |
| 158 """Send a dict MATCH command""" | |
| 159 assert self.state == "ready", "DictClient.sendMatch called when not in r
eady state" | |
| 160 self.result = None | |
| 161 self.data = None | |
| 162 self.state = "match" | |
| 163 command = "MATCH %s %s %s" % (makeAtom(database), makeAtom(strategy), ma
keAtom(word)) | |
| 164 self.sendLine(command.encode("UTF-8")) | |
| 165 | |
| 166 def dictCode_550_define(self, line): | |
| 167 """Invalid database""" | |
| 168 self.mode = "ready" | |
| 169 self.defineFailed("Invalid database") | |
| 170 | |
| 171 def dictCode_550_match(self, line): | |
| 172 """Invalid database""" | |
| 173 self.mode = "ready" | |
| 174 self.matchFailed("Invalid database") | |
| 175 | |
| 176 def dictCode_551_match(self, line): | |
| 177 """Invalid strategy""" | |
| 178 self.mode = "ready" | |
| 179 self.matchFailed("Invalid strategy") | |
| 180 | |
| 181 def dictCode_552_define(self, line): | |
| 182 """No match""" | |
| 183 self.mode = "ready" | |
| 184 self.defineFailed("No match") | |
| 185 | |
| 186 def dictCode_552_match(self, line): | |
| 187 """No match""" | |
| 188 self.mode = "ready" | |
| 189 self.matchFailed("No match") | |
| 190 | |
| 191 def dictCode_150_define(self, line): | |
| 192 """n definitions retrieved""" | |
| 193 self.result = [] | |
| 194 | |
| 195 def dictCode_151_define(self, line): | |
| 196 """Definition text follows""" | |
| 197 self.mode = "text" | |
| 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() | |
| 204 else: | |
| 205 self.result.append(Definition(word, db, dbdesc, [])) | |
| 206 self.data = [] | |
| 207 | |
| 208 def dictCode_152_match(self, line): | |
| 209 """n matches found, text follows""" | |
| 210 self.mode = "text" | |
| 211 self.result = [] | |
| 212 self.data = [] | |
| 213 | |
| 214 def dictCode_text_define(self, line): | |
| 215 """A line of definition text received""" | |
| 216 res = parseText(line) | |
| 217 if res == None: | |
| 218 self.mode = "command" | |
| 219 self.result[-1].text = self.data | |
| 220 self.data = None | |
| 221 else: | |
| 222 self.data.append(line) | |
| 223 | |
| 224 def dictCode_text_match(self, line): | |
| 225 """One line of match text received""" | |
| 226 def l(s): | |
| 227 p1, t = parseParam(s) | |
| 228 p2, t = parseParam(t) | |
| 229 return (p1, p2) | |
| 230 res = parseText(line) | |
| 231 if res == None: | |
| 232 self.mode = "command" | |
| 233 self.result = map(l, self.data) | |
| 234 self.data = None | |
| 235 else: | |
| 236 self.data.append(line) | |
| 237 | |
| 238 def dictCode_250_define(self, line): | |
| 239 """ok""" | |
| 240 t = self.result | |
| 241 self.result = None | |
| 242 self.state = "ready" | |
| 243 self.defineDone(t) | |
| 244 | |
| 245 def dictCode_250_match(self, line): | |
| 246 """ok""" | |
| 247 t = self.result | |
| 248 self.result = None | |
| 249 self.state = "ready" | |
| 250 self.matchDone(t) | |
| 251 | |
| 252 def protocolError(self, reason): | |
| 253 """override to catch unexpected dict protocol conditions""" | |
| 254 pass | |
| 255 | |
| 256 def dictConnected(self): | |
| 257 """override to be notified when the server is ready to accept commands""
" | |
| 258 pass | |
| 259 | |
| 260 def defineFailed(self, reason): | |
| 261 """override to catch reasonable failure responses to DEFINE""" | |
| 262 pass | |
| 263 | |
| 264 def defineDone(self, result): | |
| 265 """override to catch succesful DEFINE""" | |
| 266 pass | |
| 267 | |
| 268 def matchFailed(self, reason): | |
| 269 """override to catch resonable failure responses to MATCH""" | |
| 270 pass | |
| 271 | |
| 272 def matchDone(self, result): | |
| 273 """override to catch succesful MATCH""" | |
| 274 pass | |
| 275 | |
| 276 | |
| 277 class InvalidResponse(Exception): | |
| 278 pass | |
| 279 | |
| 280 | |
| 281 class DictLookup(DictClient): | |
| 282 """Utility class for a single dict transaction. To be used with DictLookupFa
ctory""" | |
| 283 | |
| 284 def protocolError(self, reason): | |
| 285 if not self.factory.done: | |
| 286 self.factory.d.errback(InvalidResponse(reason)) | |
| 287 self.factory.clientDone() | |
| 288 | |
| 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) | |
| 294 | |
| 295 def defineFailed(self, reason): | |
| 296 self.factory.d.callback([]) | |
| 297 self.factory.clientDone() | |
| 298 self.transport.loseConnection() | |
| 299 | |
| 300 def defineDone(self, result): | |
| 301 self.factory.d.callback(result) | |
| 302 self.factory.clientDone() | |
| 303 self.transport.loseConnection() | |
| 304 | |
| 305 def matchFailed(self, reason): | |
| 306 self.factory.d.callback([]) | |
| 307 self.factory.clientDone() | |
| 308 self.transport.loseConnection() | |
| 309 | |
| 310 def matchDone(self, result): | |
| 311 self.factory.d.callback(result) | |
| 312 self.factory.clientDone() | |
| 313 self.transport.loseConnection() | |
| 314 | |
| 315 | |
| 316 class DictLookupFactory(protocol.ClientFactory): | |
| 317 """Utility factory for a single dict transaction""" | |
| 318 protocol = DictLookup | |
| 319 done = None | |
| 320 | |
| 321 def __init__(self, queryType, param, d): | |
| 322 self.queryType = queryType | |
| 323 self.param = param | |
| 324 self.d = d | |
| 325 self.done = 0 | |
| 326 | |
| 327 def clientDone(self): | |
| 328 """Called by client when done.""" | |
| 329 self.done = 1 | |
| 330 del self.d | |
| 331 | |
| 332 def clientConnectionFailed(self, connector, error): | |
| 333 self.d.errback(error) | |
| 334 | |
| 335 def clientConnectionLost(self, connector, error): | |
| 336 if not self.done: | |
| 337 self.d.errback(error) | |
| 338 | |
| 339 def buildProtocol(self, addr): | |
| 340 p = self.protocol() | |
| 341 p.factory = self | |
| 342 return p | |
| 343 | |
| 344 | |
| 345 def define(host, port, database, word): | |
| 346 """Look up a word using a dict server""" | |
| 347 d = defer.Deferred() | |
| 348 factory = DictLookupFactory("define", (database, word), d) | |
| 349 | |
| 350 from twisted.internet import reactor | |
| 351 reactor.connectTCP(host, port, factory) | |
| 352 return d | |
| 353 | |
| 354 def match(host, port, database, strategy, word): | |
| 355 """Match a word using a dict server""" | |
| 356 d = defer.Deferred() | |
| 357 factory = DictLookupFactory("match", (database, strategy, word), d) | |
| 358 | |
| 359 from twisted.internet import reactor | |
| 360 reactor.connectTCP(host, port, factory) | |
| 361 return d | |
| 362 | |
| OLD | NEW |