Imported version 0.2-1
[mstardict] / src / lib / dict_client.cpp
1 /*
2  * This file part of StarDict - A international dictionary for GNOME.
3  * http://stardict.sourceforge.net
4  * Copyright (C) 2006 Evgeniy <dushistov@mail.ru>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU Library General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19  */
20
21 /*
22  * Based on RFC 2229 and gnome dictionary source code
23  */
24
25 #ifdef HAVE_CONFIG_H
26 #  include "config.h"
27 #endif
28
29 #include <glib.h>
30
31 #include "sockets.hpp"
32
33 #include "dict_client.hpp"
34
35 sigc::signal<void, const std::string&> DictClient::on_error_;
36 sigc::signal<void, const DictClient::IndexList&>
37 DictClient::on_simple_lookup_end_;
38 sigc::signal<void, const DictClient::StringList&>
39  DictClient::on_complex_lookup_end_;
40
41 /* Status codes as defined by RFC 2229 */
42 enum DICTStatusCode {
43   STATUS_INVALID                   = 0,
44
45   STATUS_N_DATABASES_PRESENT       = 110,
46   STATUS_N_STRATEGIES_PRESENT      = 111,
47   STATUS_DATABASE_INFO             = 112,
48   STATUS_HELP_TEXT                 = 113,
49   STATUS_SERVER_INFO               = 114,
50   STATUS_CHALLENGE                 = 130,
51   STATUS_N_DEFINITIONS_RETRIEVED   = 150,
52   STATUS_WORD_DB_NAME              = 151,
53   STATUS_N_MATCHES_FOUND           = 152,
54   STATUS_CONNECT                   = 220,
55   STATUS_QUIT                      = 221,
56   STATUS_AUTH_OK                   = 230,
57   STATUS_OK                        = 250,
58   STATUS_SEND_RESPONSE             = 330,
59   /* Connect response codes */
60   STATUS_SERVER_DOWN               = 420,
61   STATUS_SHUTDOWN                  = 421,
62   /* Error codes */
63   STATUS_BAD_COMMAND               = 500,
64   STATUS_BAD_PARAMETERS            = 501,
65   STATUS_COMMAND_NOT_IMPLEMENTED   = 502,
66   STATUS_PARAMETER_NOT_IMPLEMENTED = 503,
67   STATUS_NO_ACCESS                 = 530,
68   STATUS_USE_SHOW_INFO             = 531,
69   STATUS_UNKNOWN_MECHANISM         = 532,
70   STATUS_BAD_DATABASE              = 550,
71   STATUS_BAD_STRATEGY              = 551,
72   STATUS_NO_MATCH                  = 552,
73   STATUS_NO_DATABASES_PRESENT      = 554,
74   STATUS_NO_STRATEGIES_PRESENT     = 555
75 };
76
77
78 void DICT::Cmd::send(GIOChannel *channel, GError *&err)
79 {
80         g_assert(channel);
81
82         GIOStatus res =
83                 g_io_channel_write_chars(channel,
84                                          query().c_str(),
85                                          -1, NULL, &err);
86         if (res != G_IO_STATUS_NORMAL)
87                 return;
88
89         /* force flushing of the write buffer */
90         res = g_io_channel_flush(channel, &err);
91
92         if (res != G_IO_STATUS_NORMAL)
93                 return;
94
95         state_ = DICT::Cmd::DATA;
96 }
97
98 class DefineCmd : public DICT::Cmd {
99 public:
100         DefineCmd(const gchar *database, const gchar *word) {
101                 char *quote_word = g_shell_quote(word);
102                 query_ = std::string("DEFINE ") + database + ' ' + quote_word +
103                         "\r\n";
104                 g_free(quote_word);
105         }
106         bool parse(gchar *str, int code);
107 };
108
109 class MatchCmd : public DICT::Cmd {
110 public:
111         MatchCmd(const gchar *database, const gchar *strategy,
112                  const gchar *word) :
113                 database_(database),
114                 strategy_(strategy),
115                 word_(word)
116                 {               
117                 }
118         const std::string& query() {
119                 if (query_.empty()) {
120                         handle_word();
121                         char *quote_word = g_shell_quote(word_.c_str());
122                         query_ = "MATCH " + database_ + " " + strategy_ +
123                                 ' ' + quote_word + "\r\n";
124                         g_free(quote_word);
125                 }
126                 return DICT::Cmd::query();
127         }
128         bool parse(gchar *str, int code);
129 protected:
130         std::string database_;
131         std::string strategy_;
132         std::string word_;
133
134         virtual void handle_word() {}
135 };
136
137 class LevCmd : public MatchCmd {
138 public:
139         LevCmd(const gchar *database, const gchar *word) :
140                 MatchCmd(database, "lev", word)
141                 {
142                 }
143 };
144
145 class RegexpCmd : public MatchCmd {
146 public:
147         RegexpCmd(const gchar *database, const gchar *word) :
148                 MatchCmd(database, "re", word) 
149                 {
150                 }
151 protected:
152         void handle_word();
153 };
154
155 void RegexpCmd::handle_word()
156 {
157         std::string newword;
158         std::string::const_iterator it;
159
160         for (it = word_.begin(); it != word_.end(); ++it)
161                 if (*it == '*')
162                         newword += ".*";
163                 else
164                         newword += *it;
165
166         word_ = "^" + newword;
167 }
168
169 bool MatchCmd::parse(gchar *str, int code)
170 {
171         if (code == STATUS_N_MATCHES_FOUND) { 
172           gchar *p = g_utf8_strchr(str, -1, ' ');
173
174           if (p)
175             p = g_utf8_next_char (p);
176           g_debug("server replied: %d matches found\n", atoi (p));
177         } else if (0 == strcmp (str, "."))
178                 state_ = FINISH;
179         else {
180           gchar *word, *db_name, *p;
181
182           db_name = str;
183           if (!db_name)
184                   return false;
185
186           p = g_utf8_strchr(db_name, -1, ' ');
187           if (p)
188             *p = '\0';
189
190           word = g_utf8_next_char (p);
191
192           if (word[0] == '\"')
193                   word = g_utf8_next_char (word);
194
195           p = g_utf8_strchr (word, -1, '\"');
196           if (p)
197             *p = '\0';
198           
199           reslist_.push_back(DICT::Definition(word));
200         }
201
202         return true;
203 }
204
205 bool DefineCmd::parse(gchar *str, int code)
206 {
207         if (state_ != DATA)
208                 return false;
209         switch (code) {
210         case STATUS_N_DEFINITIONS_RETRIEVED: {
211                 gchar *p = g_utf8_strchr(str, -1, ' ');
212                 if (p)
213                         p = g_utf8_next_char (p);
214
215                 g_debug("server replied: %d definitions found\n", atoi(p));
216                 break;
217         }
218         case STATUS_WORD_DB_NAME: {
219                 gchar *word, *db_name, *db_full, *p;
220
221                 word = str;
222
223                 /* skip the status code */
224                 word = g_utf8_strchr(word, -1, ' ');
225                 word = g_utf8_next_char(word);
226
227                 if (word[0] == '\"')
228                         word = g_utf8_next_char(word);
229
230                 p = g_utf8_strchr(word, -1, '\"');
231                 if (p)
232                         *p = '\0';
233
234                 p = g_utf8_next_char(p);
235
236                 /* the database name is not protected by "" */
237                 db_name = g_utf8_next_char(p);
238                 if (!db_name)
239                         break;
240
241                 p = g_utf8_strchr(db_name, -1, ' ');
242                 if (p)
243                         *p = '\0';
244
245                 p = g_utf8_next_char(p);
246
247                 db_full = g_utf8_next_char(p);
248                 if (!db_full)
249                         break;
250
251                 if (db_full[0] == '\"')
252                         db_full = g_utf8_next_char(db_full);
253
254                 p = g_utf8_strchr(db_full, -1, '\"');
255                 if (p)
256                         *p = '\0';
257
258                 g_debug("{ word .= '%s', db_name .= '%s', db_full .= '%s' }\n",
259                         word, db_name, db_full);
260                 reslist_.push_back(DICT::Definition(word));
261                 break;
262         }
263         default:
264                 if (!reslist_.empty() && strcmp(".", str))
265                         reslist_.back().data_ += std::string(str) + "\n";
266
267                 break;
268         }
269         return true;
270 }
271
272
273 DictClient::DictClient(const char *host, int port)
274 {
275         host_ = host;
276         port_ = port;
277         sd_ = -1;
278         channel_ = NULL;
279         source_id_ = 0;
280         is_connected_ = false;
281         last_index_ = 0;
282 }
283
284 DictClient::~DictClient()
285 {
286         disconnect();
287 }
288
289 void DictClient::connect()
290 {
291     Socket::resolve(host_, this, on_resolved);
292 }
293
294 void DictClient::on_resolved(gpointer data, struct hostent *ret)
295 {
296         DictClient *oDictClient = (DictClient *)data;
297         oDictClient->sd_ = Socket::socket();
298
299         if (oDictClient->sd_ == -1) {
300                 on_error_.emit("Can not create socket: " + Socket::get_error_msg());
301                 return;
302         }
303
304 #ifdef _WIN32
305         oDictClient->channel_ = g_io_channel_win32_new_socket(oDictClient->sd_);
306 #else
307         oDictClient->channel_ = g_io_channel_unix_new(oDictClient->sd_);
308 #endif
309
310 /* RFC2229 mandates the usage of UTF-8, so we force this encoding */
311         g_io_channel_set_encoding(oDictClient->channel_, "UTF-8", NULL);
312
313         g_io_channel_set_line_term(oDictClient->channel_, "\r\n", 2);
314
315 /* make sure that the channel is non-blocking */
316         int flags = g_io_channel_get_flags(oDictClient->channel_);
317         flags |= G_IO_FLAG_NONBLOCK;
318         GError *err = NULL;
319         g_io_channel_set_flags(oDictClient->channel_, GIOFlags(flags), &err);
320         if (err) {
321                 g_io_channel_unref(oDictClient->channel_);
322                 oDictClient->channel_ = NULL;
323                 on_error_.emit("Unable to set the channel as non-blocking: " +
324                                std::string(err->message));
325                 g_error_free(err);
326                 return;
327         }
328
329         if (!Socket::connect(oDictClient->sd_, ret, oDictClient->port_)) {
330                 gchar *mes = g_strdup_printf("Can not connect to %s: %s\n",
331                                              oDictClient->host_.c_str(), Socket::get_error_msg().c_str());
332                 on_error_.emit(mes);
333                 g_free(mes);
334                 return;
335         }
336
337         oDictClient->source_id_ = g_io_add_watch(oDictClient->channel_, GIOCondition(G_IO_IN | G_IO_ERR),
338                                    on_io_event, oDictClient);
339 }
340
341 void DictClient::disconnect()
342 {
343         if (source_id_) {
344                 g_source_remove(source_id_);
345                 source_id_ = 0;
346         }
347
348         if (channel_) {
349                 g_io_channel_shutdown(channel_, TRUE, NULL);
350                 g_io_channel_unref(channel_);
351                 channel_ = NULL;
352         }
353         if (sd_ != -1) {
354                 Socket::close(sd_);
355                 sd_ = -1;
356         }
357         is_connected_ = false;
358 }
359
360 gboolean DictClient::on_io_event(GIOChannel *ch, GIOCondition cond,
361                                  gpointer user_data)
362 {
363         DictClient *dict_client = static_cast<DictClient *>(user_data);
364
365         g_assert(dict_client);
366
367         if (!dict_client->channel_) {
368                 g_warning("No channel available\n");
369                 return FALSE;
370         }
371
372         if (cond & G_IO_ERR) {
373                 gchar *mes =
374                         g_strdup_printf("Connection failed to the dictionary server at %s:%d",
375                                         dict_client->host_.c_str(), dict_client->port_);
376                 on_error_.emit(mes);
377                 g_free(mes);
378                 return FALSE;
379         }
380
381         GError *err = NULL;
382         gsize term, len;
383         gchar *line;
384         GIOStatus res;
385
386         for (;;) {
387                 if (!dict_client->channel_)
388                         break;
389                 res = g_io_channel_read_line(dict_client->channel_, &line,
390                                              &len, &term, &err);
391                 if (res == G_IO_STATUS_ERROR) {
392                         if (err) {
393                                 on_error_.emit("Error while reading reply from server: " +
394                                                std::string(err->message));
395                                 g_error_free(err);
396                         }
397                         dict_client->disconnect();
398
399                         return FALSE;
400                 }
401
402                 if (!len)
403                         break;
404
405                 //truncate the line terminator before parsing
406                 line[term] = '\0';
407                 int status_code = get_status_code(line);
408                 bool res = dict_client->parse(line, status_code);
409                 g_free(line);
410                 if (!res) {
411                         dict_client->disconnect();
412                         return FALSE;
413                 }
414         }
415
416         return TRUE;
417 }
418
419 bool DictClient::parse(gchar *line, int status_code)
420 {
421         g_debug("get %s\n", line);
422
423         if (!cmd_.get()) {
424                 if (status_code == STATUS_CONNECT)
425                         is_connected_ = true;
426                 else if (status_code == STATUS_SERVER_DOWN ||
427                          status_code == STATUS_SHUTDOWN) {
428                         gchar *mes =
429                                 g_strdup_printf("Unable to connect to the "
430                                                 "dictionary server at '%s:%d'. "
431                                                 "The server replied with code"
432                                                 " %d (server down)",
433                                                 host_.c_str(), port_,
434                                                 status_code);
435                         on_error_.emit(mes);
436                         g_free(mes);
437                         return true;
438                 } else {
439                         gchar *mes =
440                                 g_strdup_printf("Unable to parse the dictionary"
441                                                 " server reply: '%s'", line);
442                         on_error_.emit(mes);
443                         g_free(mes);
444                         return false;
445                 }
446         }
447
448         bool success = false;
449
450         switch (status_code) {
451         case STATUS_BAD_PARAMETERS:
452         {
453                 gchar *mes = g_strdup_printf("Bad parameters for command '%s'",
454                                              cmd_->query().c_str());
455                 on_error_.emit(mes);
456                 g_free(mes);
457                 cmd_->state_ = DICT::Cmd::FINISH;
458                 break;
459         }
460         case STATUS_BAD_COMMAND:
461         {
462                 gchar *mes = g_strdup_printf("Bad command '%s'",
463                                              cmd_->query().c_str());
464                 on_error_.emit(mes);
465                 g_free(mes);
466                 cmd_->state_ = DICT::Cmd::FINISH;
467                 break;
468         }
469         default:
470                 success = true;
471                 break;
472         }
473
474         if (cmd_->state_ == DICT::Cmd::START) {
475                 GError *err = NULL;
476                 cmd_->send(channel_, err);
477                 if (err) {
478                         on_error_.emit(err->message);
479                         g_error_free(err);
480                         return false;
481                 }
482                 return true;
483         }
484
485         if (status_code == STATUS_OK || cmd_->state_ == DICT::Cmd::FINISH ||
486             status_code == STATUS_NO_MATCH ||
487             status_code == STATUS_BAD_DATABASE ||
488             status_code == STATUS_BAD_STRATEGY ||
489             status_code == STATUS_NO_DATABASES_PRESENT ||
490             status_code == STATUS_NO_STRATEGIES_PRESENT) {
491                 defmap_.clear();
492                 const DICT::DefList& res = cmd_->result();
493                 if (simple_lookup_) {
494                         IndexList ilist(res.size());
495                         for (size_t i = 0; i < res.size(); ++i) {
496                                 ilist[i] = last_index_;
497                                 defmap_.insert(std::make_pair(last_index_++, res[i]));
498                         }
499                         last_index_ = 0;
500                         cmd_.reset(0);
501                         disconnect();
502                         on_simple_lookup_end_.emit(ilist);
503                 } else {
504                         StringList slist;
505                         for (size_t i = 0; i < res.size(); ++i)
506                                 slist.push_back(res[i].word_);
507                         last_index_ = 0;
508                         cmd_.reset(0);
509                         disconnect();
510                         on_complex_lookup_end_.emit(slist);
511                 }
512
513                 return success;
514         }
515
516         if (!cmd_->parse(line, status_code))
517                 return false;
518
519
520         return true;
521 }
522
523 /* retrieve the status code from the server response line */
524 int DictClient::get_status_code(gchar *line)
525 {
526   gint retval;
527
528   if (strlen (line) < 3)
529     return 0;
530
531   if (!g_unichar_isdigit (line[0]) ||
532       !g_unichar_isdigit (line[1]) ||
533       !g_unichar_isdigit (line[2]))
534     return 0;
535
536   gchar tmp = line[3];
537   line[3] = '\0';
538
539   retval = atoi(line);
540
541   line[3] = tmp;
542
543   return retval;
544 }
545
546 void DictClient::lookup_simple(const gchar *word)
547 {
548         simple_lookup_ = true;
549
550         if (!word || !*word) {
551                 on_simple_lookup_end_.emit(IndexList());
552                 return;
553         }
554
555         if (!channel_ || !source_id_)
556                         return;
557     connect();
558
559         cmd_.reset(new DefineCmd("*", word));
560 }
561
562 void DictClient::lookup_with_rule(const gchar *word)
563 {
564         simple_lookup_ = false;
565
566         if (!word || !*word) {
567                 on_complex_lookup_end_.emit(StringList());
568                 return;
569         }
570
571         if (!channel_ || !source_id_)
572                         return;
573     connect();
574
575         cmd_.reset(new RegexpCmd("*", word));
576 }
577
578 void DictClient::lookup_with_fuzzy(const gchar *word)
579 {
580         simple_lookup_ = false;
581
582         if (!word || !*word) {
583                 on_complex_lookup_end_.emit(StringList());
584                 return;
585         }
586
587         if (!channel_ || !source_id_)
588                         return;
589     connect();
590
591         cmd_.reset(new LevCmd("*", word));
592 }
593
594 const gchar *DictClient::get_word(size_t index) const
595 {
596         DefMap::const_iterator it = defmap_.find(index);
597
598         if (it == defmap_.end())
599                 return NULL;
600         return it->second.word_.c_str();
601 }
602
603 const gchar *DictClient::get_word_data(size_t index) const
604 {
605         DefMap::const_iterator it = defmap_.find(index);
606
607         if (it == defmap_.end())
608                 return NULL;
609         return it->second.data_.c_str();
610 }
611