1 /*
2 * edbflatfile.cpp - asynchronous I/O event database
3 * Copyright (C) 2001, 2002 Justin Karneges
4 *
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this library; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 *
19 */
20
21 #include <QVector>
22 #include <QFileInfo>
23 #include <QDir>
24 #include <QTimer>
25 #include <QTextStream>
26 #include <QDateTime>
27
28 #include "edbflatfile.h"
29 #include "psicon.h"
30 #include "psiaccount.h"
31 #include "psicontactlist.h"
32 #include "xmpp_jid.h"
33 #include "jidutil.h"
34 #include "common.h"
35 #include "applicationinfo.h"
36
37 #define FAKEDELAY 0
38
39 using namespace XMPP;
40
41 //----------------------------------------------------------------------------
42 // EDBFlatFile
43 //----------------------------------------------------------------------------
44 struct item_file_req
45 {
46 Jid j;
47 int type; // 0 = latest, 1 = oldest, 2 = random, 3 = write
48 int start;
49 int len;
50 int dir;
51 int id;
52 QDateTime date;
53 QString findStr;
54 PsiEvent::Ptr event;
55
56 enum Type {
57 Type_get,
58 Type_append,
59 Type_find,
60 Type_erase
61 };
62 };
63
64 class EDBFlatFile::Private
65 {
66 public:
Private()67 Private() {}
68
69 QList<File*> flist;
70 QList<item_file_req*> rlist;
71 };
72
EDBFlatFile(PsiCon * psi)73 EDBFlatFile::EDBFlatFile(PsiCon *psi)
74 : EDB(psi)
75 {
76 d = new Private;
77 }
78
~EDBFlatFile()79 EDBFlatFile::~EDBFlatFile()
80 {
81 qDeleteAll(d->rlist);
82 qDeleteAll(d->flist);
83 d->flist.clear();
84
85 delete d;
86 }
87
features() const88 int EDBFlatFile::features() const
89 {
90 return 0;
91 }
92
get(const QString &,const Jid & j,const QDateTime date,int direction,int start,int len)93 int EDBFlatFile::get(const QString &/*accId*/, const Jid &j, const QDateTime date, int direction, int start, int len)
94 {
95 item_file_req *r = new item_file_req;
96 r->j = j;
97 r->type = item_file_req::Type_get;
98 r->start = start;
99 r->len = len < 1 ? 1: len;
100 r->dir = direction;
101 r->date = date;
102 r->id = genUniqueId();
103 d->rlist.append(r);
104
105 QTimer::singleShot(FAKEDELAY, this, SLOT(performRequests()));
106 return r->id;
107 }
108
find(const QString &,const QString & str,const Jid & j,const QDateTime date,int direction)109 int EDBFlatFile::find(const QString &/*accId*/, const QString &str, const Jid &j, const QDateTime date, int direction)
110 {
111 item_file_req *r = new item_file_req;
112 r->j = j;
113 r->type = item_file_req::Type_find;
114 r->len = 1;
115 r->dir = direction;
116 r->findStr = str;
117 r->date = date;
118 r->id = genUniqueId();
119 d->rlist.append(r);
120
121 QTimer::singleShot(FAKEDELAY, this, SLOT(performRequests()));
122 return r->id;
123 }
124
append(const QString &,const Jid & j,const PsiEvent::Ptr & e,int type)125 int EDBFlatFile::append(const QString &/*accId*/, const Jid &j, const PsiEvent::Ptr &e, int type)
126 {
127 if (type != EDB::Contact)
128 return 0;
129 item_file_req *r = new item_file_req;
130 r->j = j;
131 r->type = item_file_req::Type_append;
132 r->event = e;
133 if ( !r->event ) {
134 qWarning("EDBFlatFile::append(): Attempted to append incompatible type.");
135 delete r;
136 return 0;
137 }
138 r->id = genUniqueId();
139 d->rlist.append(r);
140
141 QTimer::singleShot(FAKEDELAY, this, SLOT(performRequests()));
142 return r->id;
143 }
144
erase(const QString &,const Jid & j)145 int EDBFlatFile::erase(const QString &/*accId*/, const Jid &j)
146 {
147 item_file_req *r = new item_file_req;
148 r->j = j;
149 r->type = item_file_req::Type_erase;
150 r->id = genUniqueId();
151 d->rlist.append(r);
152
153 QTimer::singleShot(FAKEDELAY, this, SLOT(performRequests()));
154 return r->id;
155 }
156
contacts(const QString & accId,int type)157 QList<EDB::ContactItem> EDBFlatFile::contacts(const QString &accId, int type)
158 {
159 if (!accId.isEmpty())
160 return File::contacts(accId, type);
161 return File::contacts(psi()->contactList()->defaultAccount()->id(), type);
162 }
163
eventsCount(const QString & accId,const XMPP::Jid & jid)164 quint64 EDBFlatFile::eventsCount(const QString &accId, const XMPP::Jid &jid)
165 {
166 quint64 res = 0;
167 if (!jid.isEmpty())
168 res = ensureFile(jid)->total();
169 else
170 foreach (const ContactItem &ci, contacts(accId, Contact))
171 res += ensureFile(ci.jid)->total();
172 return res;
173 }
174
findFile(const Jid & j) const175 EDBFlatFile::File *EDBFlatFile::findFile(const Jid &j) const
176 {
177 foreach(File* i, d->flist) {
178 if(i->j.compare(j, false))
179 return i;
180 }
181 return 0;
182 }
183
ensureFile(const Jid & j)184 EDBFlatFile::File *EDBFlatFile::ensureFile(const Jid &j)
185 {
186 File *i = findFile(j);
187 if(!i) {
188 i = new File(Jid(j.bare()));
189 connect(i, SIGNAL(timeout()), SLOT(file_timeout()));
190 d->flist.append(i);
191 }
192 return i;
193 }
194
deleteFile(const Jid & j)195 bool EDBFlatFile::deleteFile(const Jid &j)
196 {
197 File *i = findFile(j);
198
199 QString fname;
200
201 if (i) {
202 fname = i->fname;
203 d->flist.removeAll(i);
204 delete i;
205 }
206 else {
207 fname = File::jidToFileName(j);
208 }
209
210 QFileInfo fi(fname);
211 if(fi.exists()) {
212 QDir dir = fi.dir();
213 return dir.remove(fi.fileName());
214 }
215 else
216 return true;
217 }
218
performRequests()219 void EDBFlatFile::performRequests()
220 {
221 if(d->rlist.isEmpty())
222 return;
223
224 item_file_req *r = d->rlist.takeFirst();
225
226 File *f = ensureFile(r->j);
227 int type = r->type;
228 if(type == item_file_req::Type_get) {
229 EDBResult result;
230 int startId = 0;
231 int direction = r->dir;
232 int id = f->getId(r->date, direction, r->start);
233 if (id != -1) {
234 int len;
235 if(direction == Forward) {
236 if(id + r->len > f->total())
237 len = f->total() - id;
238 else
239 len = r->len;
240 }
241 else {
242 if((id+1) - r->len < 0)
243 len = id+1;
244 else
245 len = r->len;
246 }
247
248 startId = id;
249 for(int n = 0; n < len; ++n) {
250 PsiEvent::Ptr e(f->get(id));
251 if(e) {
252 EDBItemPtr ei = EDBItemPtr(new EDBItem(e, QString::number(id)));
253 result.append(ei);
254 }
255
256 if(direction == Forward)
257 ++id;
258 else
259 --id;
260 }
261 if (direction == Backward)
262 startId = id + 1;
263 }
264 resultReady(r->id, result, startId);
265 }
266 else if(type == item_file_req::Type_append) {
267 writeFinished(r->id, f->append(r->event));
268 }
269 else if(type == item_file_req::Type_find) {
270 int id = f->getId(r->date, r->dir, 0);
271 EDBResult result;
272 if (id != -1) {
273 while (1) {
274 PsiEvent::Ptr e(f->get(id));
275 if (!e)
276 break;
277
278 if(e->type() == PsiEvent::Message) {
279 MessageEvent::Ptr me = e.staticCast<MessageEvent>();
280 const Message &m = me->message();
281 if(m.body().indexOf(r->findStr, 0, Qt::CaseInsensitive) != -1) {
282 EDBItemPtr ei = EDBItemPtr(new EDBItem(e, QString::number(id)));
283 result.append(ei);
284 //commented line below to return ALL(instead of just first) messages that contain findStr
285 //break;
286 }
287 }
288 if(r->dir == Forward)
289 ++id;
290 else
291 --id;
292 }
293 }
294 resultReady(r->id, result, 0);
295 }
296 else if(type == item_file_req::Type_erase) {
297 writeFinished(r->id, deleteFile(f->j));
298 }
299 else {
300 qWarning("EDBFlatFile::performRequests(): Invalid type.");
301 }
302
303 delete r;
304 }
305
file_timeout()306 void EDBFlatFile::file_timeout()
307 {
308 File *i = (File *)sender();
309 d->flist.removeAll(i);
310 i->deleteLater();
311 }
312
313
314 //----------------------------------------------------------------------------
315 // EDBFlatFile::File
316 //----------------------------------------------------------------------------
317 class EDBFlatFile::File::Private
318 {
319 public:
Private()320 Private() {}
321
322 QVector<quint64> index;
323 bool indexed;
324 };
325
File(const Jid & _j)326 EDBFlatFile::File::File(const Jid &_j)
327 {
328 d = new Private;
329 d->indexed = false;
330
331 j = _j;
332 valid = false;
333 t = new QTimer(this);
334 connect(t, SIGNAL(timeout()), SLOT(timer_timeout()));
335
336 //printf("[EDB opening -- %s]\n", j.full().latin1());
337 fname = jidToFileName(_j);
338 f.setFileName(fname);
339 valid = f.open(QIODevice::ReadWrite);
340
341 touch();
342 }
343
~File()344 EDBFlatFile::File::~File()
345 {
346 if(valid)
347 f.close();
348 //printf("[EDB closing -- %s]\n", j.full().latin1());
349
350 delete d;
351 }
352
jidToFileName(const XMPP::Jid & j)353 QString EDBFlatFile::File::jidToFileName(const XMPP::Jid &j)
354 {
355 return ApplicationInfo::historyDir() + "/" + strToFileName(JIDUtil::encode(j.bare()).toLower());
356 }
357
strToFileName(const QString & s)358 QString EDBFlatFile::File::strToFileName(const QString &s)
359 {
360 QFileInfo fi(s);
361 return fi.fileName() + ".history";
362 }
363
contacts(const QString & accId,int type)364 QList<EDB::ContactItem> EDBFlatFile::File::contacts(const QString &accId, int type)
365 {
366 QList<ContactItem> res;
367 if (type == EDB::Contact) {
368 QDir dir(ApplicationInfo::historyDir() + "/");
369 QFileInfoList flist = dir.entryInfoList(QStringList(strToFileName("*")), QDir::Files);
370 foreach (const QFileInfo &fi, flist) {
371 XMPP::Jid jid(JIDUtil::decode(fi.completeBaseName()));
372 if (jid.isValid())
373 res.append(ContactItem(accId, jid));
374 }
375 }
376 return res;
377 }
378
ensureIndex()379 void EDBFlatFile::File::ensureIndex()
380 {
381 if ( valid && !d->indexed ) {
382 if (f.isSequential()) {
383 qWarning("EDBFlatFile::File::ensureIndex(): Can't index sequential files.");
384 return;
385 }
386
387 f.reset(); // go to beginning
388 d->index.clear();
389
390 //printf(" file: %s\n", fname.latin1());
391 // build index
392 while(1) {
393 quint64 at = f.pos();
394
395 // locate a newline
396 bool found = false;
397 char c;
398 while (f.getChar(&c)) {
399 if (c == '\n') {
400 found = true;
401 break;
402 }
403 }
404
405 if(!found)
406 break;
407
408 int oldsize = d->index.size();
409 d->index.resize(oldsize+1);
410 d->index[oldsize] = at;
411 }
412
413 d->indexed = true;
414 }
415 else {
416 //printf(" file: can't open\n");
417 }
418
419 //printf(" messages: %d\n\n", d->index.size());
420 }
421
total() const422 int EDBFlatFile::File::total() const
423 {
424 ((EDBFlatFile::File *)this)->ensureIndex();
425 return d->index.size();
426 }
427
getId(QDateTime & date,int dir,int offset)428 int EDBFlatFile::File::getId(QDateTime &date, int dir, int offset)
429 {
430 if (date.isNull()) {
431 if (dir == EDBFlatFile::Forward)
432 return offset;
433 if (offset >= total())
434 return 0;
435 return total() - offset - 1;
436 }
437 ensureIndex();
438 if (total() == 0)
439 return 0;
440 int id = findNearestDate(date);
441 if (id == -1)
442 return -1;
443
444 QDateTime fDate = getDate(id);
445 if (!fDate.isValid())
446 return -1;
447
448 if (dir == EDBFlatFile::Forward) {
449 if (fDate < date)
450 ++id;
451 id += offset;
452 }
453 else {
454 if (fDate > date)
455 --id;
456 id -= offset;
457 }
458 if (id >= total())
459 id = total() - 1;
460 else if (id < 0)
461 id = 0;
462 return id;
463 }
464
465 /*
466 * This method returns an index of a string with the event
467 * which has the nearest date to the specified one.
468 * Returned date may be earlier than that is passed as an argument.
469 */
findNearestDate(const QDateTime & date)470 int EDBFlatFile::File::findNearestDate(const QDateTime &date)
471 {
472 int cnt = total();
473 if (cnt == 0)
474 return 0;
475
476 // Binary search algorithm
477 int left = 0;
478 int right = cnt;
479 while (right - left > 0) {
480 int idx = left + (right - left) / 2;
481 const QDateTime mid = getDate(idx);
482 if (!mid.isValid())
483 return -1;
484 if (date <= mid)
485 right = idx;
486 else
487 left = idx + 1;
488 }
489 // --
490 if (right == cnt) // Specified date is later than the latest one in the history
491 return cnt - 1;
492
493 // Now `right` is pointing to the index with an identical or later date
494 while (right > 0) { // in case of there are more than one identical date
495 const QDateTime dt = getDate(right - 1);
496 if (!dt.isValid())
497 return -1;
498 if (dt != date)
499 break;
500 --right;
501 }
502 if (right == 0)
503 return 0;
504
505 const QDateTime dt1 = getDate(right - 1);
506 const QDateTime dt2 = getDate(right);
507 if (!dt1.isValid() || !dt2.isValid())
508 return -1;
509 if (dt1.secsTo(date) <= date.secsTo(dt2)) // compares with earlier one
510 --right;
511 return right;
512 }
513
touch()514 void EDBFlatFile::File::touch()
515 {
516 t->start(30000);
517 }
518
timer_timeout()519 void EDBFlatFile::File::timer_timeout()
520 {
521 timeout();
522 }
523
get(int id)524 PsiEvent::Ptr EDBFlatFile::File::get(int id)
525 {
526 QString line = getLine(id);
527 if (line.isNull())
528 return PsiEvent::Ptr();
529 PsiEvent::Ptr res = lineToEvent(line);
530 if (!res)
531 qWarning("EDBFlatFile::File::get() Failed to parse file %s, line %d", fname.toLatin1().data(), id + 1);
532 return res;
533 }
534
append(const PsiEvent::Ptr & e)535 bool EDBFlatFile::File::append(const PsiEvent::Ptr &e)
536 {
537 touch();
538
539 if(!valid)
540 return false;
541
542 QString line = eventToLine(e);
543 if(line.isEmpty())
544 return false;
545
546 f.seek(f.size());
547 quint64 at = f.pos();
548
549 QTextStream t;
550 t.setDevice(&f);
551 t.setCodec("UTF-8");
552 t << line << endl;
553 f.flush();
554
555 if ( d->indexed ) {
556 int oldsize = d->index.size();
557 d->index.resize(oldsize+1);
558 d->index[oldsize] = at;
559 }
560
561 return true;
562 }
563
lineToEvent(const QString & line)564 PsiEvent::Ptr EDBFlatFile::File::lineToEvent(const QString &line)
565 {
566 // -- parse the line --
567 enum { Time = 0, Type = 1, Origin = 2, Flags = 3, Subj = 4, UrlAddr = 5, UrlDesc = 6 };
568 QStringList strData;
569 int x1 = line.indexOf('|');
570 if (x1 != -1) {
571 ++x1;
572 for (int i = 0; i <= UrlDesc; ++i) // Filing default data
573 strData << QString();
574 int max = Flags;
575 for (int idx = 0; idx <= max; ) {
576 int x2 = line.indexOf('|', x1);
577 if (x2 == -1) {
578 x1 = -1;
579 break;
580 }
581 QString s = line.mid(x1, x2 - x1);
582 strData[idx] = s;
583 x1 = x2 + 1;
584
585 if (idx == Flags) { // check for extra fields
586 if (s.length() < 2) {
587 x1 = -1;
588 break;
589 }
590 if (s.at(1) != '-') {
591 int subflag = QString(s.at(1)).toInt(NULL, 16);
592 if (subflag & 1) // have subject?
593 max = Subj;
594 else // Skip subject
595 ++idx;
596 if (subflag & 2) // have url?
597 max = UrlDesc;
598 }
599 }
600 ++idx;
601 }
602 }
603
604 if (x1 == -1)
605 return PsiEvent::Ptr();
606
607 // body text is last
608 QString sText = line.mid(x1);
609
610 // -- read end --
611
612 int type = strData.at(Type).toInt();
613 if(type == 0 || type == 1 || type == 4 || type == 5) {
614 Message m;
615 m.setTimeStamp(QDateTime::fromString(strData.at(Time), Qt::ISODate));
616 if(type == 1)
617 m.setType("chat");
618 else if(type == 4)
619 m.setType("error");
620 else if(type == 5)
621 m.setType("headline");
622 else
623 m.setType("");
624
625 bool originLocal = (strData.at(Origin) == "to") ? true: false;
626 m.setFrom(j);
627 if (strData.at(Flags).at(0) == 'N')
628 m.setBody(logdecode(sText));
629 else
630 m.setBody(logdecode(QString::fromUtf8(sText.toLatin1())));
631 m.setSubject(logdecode(strData.at(Subj)));
632
633 QString url = logdecode(strData.at(UrlAddr));
634 if(!url.isEmpty())
635 m.urlAdd(Url(url, logdecode(strData.at(UrlDesc))));
636 m.setSpooled(true);
637
638 MessageEvent::Ptr me(new MessageEvent(m, 0));
639 me->setOriginLocal(originLocal);
640
641 return me.staticCast<PsiEvent>();
642 }
643 else if(type == 2 || type == 3 || type == 6 || type == 7 || type == 8) {
644 QString subType = "subscribe";
645 if(type == 2) {
646 // stupid "system message" from Psi <= 0.8.6
647 // try to figure out what kind it REALLY is based on the text
648 if(sText == tr("<big>[System Message]</big><br>You are now authorized."))
649 subType = "subscribed";
650 else if(sText == tr("<big>[System Message]</big><br>Your authorization has been removed!"))
651 subType = "unsubscribed";
652 }
653 else if(type == 3)
654 subType = "subscribe";
655 else if(type == 6)
656 subType = "subscribed";
657 else if(type == 7)
658 subType = "unsubscribe";
659 else if(type == 8)
660 subType = "unsubscribed";
661
662 AuthEvent::Ptr ae(new AuthEvent(j, subType, 0));
663 ae->setTimeStamp(QDateTime::fromString(strData.at(Time), Qt::ISODate));
664 return ae.staticCast<PsiEvent>();
665 }
666
667 return PsiEvent::Ptr();
668 }
669
eventToLine(const PsiEvent::Ptr & e)670 QString EDBFlatFile::File::eventToLine(const PsiEvent::Ptr &e)
671 {
672 int subflags = 0;
673 QString sTime, sType, sOrigin, sFlags;
674
675 if(e->type() == PsiEvent::Message) {
676 MessageEvent::Ptr me = e.staticCast<MessageEvent>();
677 const Message &m = me->message();
678 const UrlList urls = m.urlList();
679
680 if(!m.subject().isEmpty())
681 subflags |= 1;
682 if(!urls.isEmpty())
683 subflags |= 2;
684
685 sTime = m.timeStamp().toString(Qt::ISODate);
686 int n = 0;
687 if(m.type() == "chat")
688 n = 1;
689 else if(m.type() == "error")
690 n = 4;
691 else if(m.type() == "headline")
692 n = 5;
693 sType.setNum(n);
694 sOrigin = e->originLocal() ? "to": "from";
695 sFlags = "N---";
696
697 if(subflags != 0)
698 sFlags[1] = QString::number(subflags,16)[0];
699
700 // | date | type | To/from | flags | text
701 QString line = "|" + sTime + "|" + sType + "|" + sOrigin + "|" + sFlags + "|";
702
703 if(subflags & 1) {
704 line += logencode(m.subject()) + "|";
705 }
706 if(subflags & 2) {
707 const Url &url = urls.first();
708 line += logencode(url.url()) + "|";
709 line += logencode(url.desc()) + "|";
710 }
711 line += logencode(m.body());
712
713 return line;
714 }
715 else if(e->type() == PsiEvent::Auth) {
716 AuthEvent::Ptr ae = e.staticCast<AuthEvent>();
717 sTime = ae->timeStamp().toString(Qt::ISODate);
718 QString subType = ae->authType();
719 int n = 0;
720 if(subType == "subscribe")
721 n = 3;
722 else if(subType == "subscribed")
723 n = 6;
724 else if(subType == "unsubscribe")
725 n = 7;
726 else if(subType == "unsubscribed")
727 n = 8;
728 sType.setNum(n);
729 sOrigin = e->originLocal() ? "to": "from";
730 sFlags = "N---";
731
732 // | date | type | To/from | flags | text
733 QString line = "|" + sTime + "|" + sType + "|" + sOrigin + "|" + sFlags + "|";
734 line += logencode(subType);
735
736 return line;
737 }
738
739 return "";
740 }
741
getLine(int id)742 QString EDBFlatFile::File::getLine(int id)
743 {
744 touch();
745
746 if(!valid)
747 return QString();
748
749 ensureIndex();
750 if(id < 0 || id >= (int)d->index.size())
751 return QString();
752
753 f.seek(d->index[id]);
754
755 QTextStream t;
756 t.setDevice(&f);
757 t.setCodec("UTF-8");
758 return t.readLine();
759 }
760
getDate(int id)761 QDateTime EDBFlatFile::File::getDate(int id)
762 {
763 QString line = getLine(id);
764 if (line.isNull())
765 return QDateTime();
766
767 int p1 = line.indexOf('|') + 1;
768 if (p1 == -1)
769 return QDateTime();
770 int p2 = line.indexOf('|', p1);
771 if (p2 == -1)
772 return QDateTime();
773
774 QString sTime = line.mid(p1, p2 - p1);
775 return QDateTime::fromString(sTime, Qt::ISODate);
776 }
777