Fixes: Avoid infinite delay of update timers
[qtcontacts-tracker:hasselmms-contactsd.git] / plugins / telepathy / cdtpstorage.cpp
1 /** This file is part of Contacts daemon
2  **
3  ** Copyright (c) 2010-2011 Nokia Corporation and/or its subsidiary(-ies).
4  **
5  ** Contact:  Nokia Corporation (info@qt.nokia.com)
6  **
7  ** GNU Lesser General Public License Usage
8  ** This file may be used under the terms of the GNU Lesser General Public License
9  ** version 2.1 as published by the Free Software Foundation and appearing in the
10  ** file LICENSE.LGPL included in the packaging of this file.  Please review the
11  ** following information to ensure the GNU Lesser General Public License version
12  ** 2.1 requirements will be met:
13  ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
14  **
15  ** In addition, as a special exception, Nokia gives you certain additional rights.
16  ** These rights are described in the Nokia Qt LGPL Exception version 1.1, included
17  ** in the file LGPL_EXCEPTION.txt in this package.
18  **
19  ** Other Usage
20  ** Alternatively, this file may be used in accordance with the terms and
21  ** conditions contained in a signed written agreement between you and Nokia.
22  **/
23
24 #include <tracker-sparql.h>
25
26 #include <TelepathyQt4/AvatarData>
27 #include <TelepathyQt4/ContactCapabilities>
28 #include <TelepathyQt4/ConnectionCapabilities>
29 #include <qtcontacts-tracker/phoneutils.h>
30 #include <qtcontacts-tracker/garbagecollector.h>
31 #include <ontologies.h>
32
33 #include <importstateconst.h>
34
35 #include "cdtpstorage.h"
36 #include "debug.h"
37
38 CUBI_USE_NAMESPACE_RESOURCES
39 using namespace Contactsd;
40
41 static const LiteralValue defaultGenerator = LiteralValue(QString::fromLatin1("telepathy"));
42 static const ResourceValue defaultGraph = ResourceValue(QString::fromLatin1("urn:uuid:08070f5c-a334-4d19-a8b0-12a3071bfab9"));
43 static const ResourceValue privateGraph = ResourceValue(QString::fromLatin1("urn:uuid:679293d4-60f0-49c7-8d63-f1528fe31f66"));
44 static const ResourceValue aValue = ResourceValue(QString::fromLatin1("a"), ResourceValue::PrefixedName);
45 static const Variable imAddressVar = Variable(QString::fromLatin1("imAddress"));
46 static const Variable imContactVar = Variable(QString::fromLatin1("imContact"));
47 static const Variable imAccountVar = Variable(QString::fromLatin1("imAccount"));
48 static const ValueChain imAddressChain = ValueChain() << nco::hasAffiliation::resource() << nco::hasIMAddress::resource();
49
50 CDTpStorage::CDTpStorage(QObject *parent) : QObject(parent),
51     mUpdateRunning(false), mDirectGC(false)
52 {
53     mUpdateTimer.setInterval(0);
54     mUpdateTimer.setSingleShot(true);
55     connect(&mUpdateTimer, SIGNAL(timeout()), SLOT(onUpdateQueueTimeout()));
56
57     if (!qgetenv("CONTACTSD_DIRECT_GC").isEmpty())
58         mDirectGC = true;
59 }
60
61 CDTpStorage::~CDTpStorage()
62 {
63 }
64
65 static void deletePropertyWithGraph(Delete &d, const Value &s, const Value &p, const Variable &g)
66 {
67     Variable o;
68     d.addData(s, p, o);
69
70     Graph graph(g);
71     graph.addPattern(s, p, o);
72
73     PatternGroup optional;
74     optional.setOptional(true);
75     optional.addPattern(graph);
76
77     d.addRestriction(optional);
78 }
79
80 static void deleteProperty(Delete &d, const Value &s, const Value &p)
81 {
82     Variable o;
83     d.addData(s, p, o);
84
85     PatternGroup optional;
86     optional.setOptional(true);
87     optional.addPattern(s, p, o);
88
89     d.addRestriction(optional);
90 }
91
92 static ResourceValue presenceType(Tp::ConnectionPresenceType presenceType)
93 {
94     switch (presenceType) {
95     case Tp::ConnectionPresenceTypeUnset:
96         return nco::presence_status_unknown::resource();
97     case Tp::ConnectionPresenceTypeOffline:
98         return nco::presence_status_offline::resource();
99     case Tp::ConnectionPresenceTypeAvailable:
100         return nco::presence_status_available::resource();
101     case Tp::ConnectionPresenceTypeAway:
102         return nco::presence_status_away::resource();
103     case Tp::ConnectionPresenceTypeExtendedAway:
104         return nco::presence_status_extended_away::resource();
105     case Tp::ConnectionPresenceTypeHidden:
106         return nco::presence_status_hidden::resource();
107     case Tp::ConnectionPresenceTypeBusy:
108         return nco::presence_status_busy::resource();
109     case Tp::ConnectionPresenceTypeUnknown:
110         return nco::presence_status_unknown::resource();
111     case Tp::ConnectionPresenceTypeError:
112         return nco::presence_status_error::resource();
113     default:
114         break;
115     }
116
117     warning() << "Unknown telepathy presence status" << presenceType;
118
119     return nco::presence_status_error::resource();
120 }
121
122 static ResourceValue presenceState(Tp::Contact::PresenceState presenceState)
123 {
124     switch (presenceState) {
125     case Tp::Contact::PresenceStateNo:
126         return nco::predefined_auth_status_no::resource();
127     case Tp::Contact::PresenceStateAsk:
128         return nco::predefined_auth_status_requested::resource();
129     case Tp::Contact::PresenceStateYes:
130         return nco::predefined_auth_status_yes::resource();
131     }
132
133     warning() << "Unknown telepathy presence state:" << presenceState;
134
135     return nco::predefined_auth_status_no::resource();
136 }
137
138 static LiteralValue literalTimeStamp()
139 {
140     return LiteralValue(QDateTime::currentDateTime());
141 }
142
143 static QString imAddress(const QString &accountPath, const QString &contactId)
144 {
145     static const QString tmpl = QString::fromLatin1("telepathy:%1!%2");
146     return tmpl.arg(accountPath, contactId);
147 }
148
149 static QString imAddress(const QString &accountPath)
150 {
151     static const QString tmpl = QString::fromLatin1("telepathy:%1!self");
152     return tmpl.arg(accountPath);
153 }
154
155 static QString imAccount(const QString &accountPath)
156 {
157     static const QString tmpl = QString::fromLatin1("telepathy:%1");
158     return tmpl.arg(accountPath);
159 }
160
161 static ResourceValue literalIMAddress(const QString &accountPath, const QString &contactId)
162 {
163     return ResourceValue(imAddress(accountPath, contactId));
164 }
165
166 static ResourceValue literalIMAddress(const CDTpContactPtr &contactWrapper)
167 {
168     const QString accountPath = contactWrapper->accountWrapper()->account()->objectPath();
169     const QString contactId = contactWrapper->contact()->id();
170     return ResourceValue(imAddress(accountPath, contactId));
171 }
172
173 static ResourceValue literalIMAddress(const CDTpAccountPtr &accountWrapper)
174 {
175     return ResourceValue(imAddress(accountWrapper->account()->objectPath()));
176 }
177
178 static ValueList literalIMAddressList(const QList<CDTpContactPtr> &contacts)
179 {
180     ValueList list;
181     Q_FOREACH (const CDTpContactPtr &contactWrapper, contacts) {
182         const QString accountPath = contactWrapper->accountWrapper()->account()->objectPath();
183         const QString contactId = contactWrapper->contact()->id();
184         list.addValue(LiteralValue(imAddress(accountPath, contactId)));
185     }
186     return list;
187 }
188
189 static ValueList literalIMAddressList(const QList<CDTpAccountPtr> &accounts)
190 {
191     ValueList list;
192     Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
193         list.addValue(LiteralValue(imAddress(accountWrapper->account()->objectPath())));
194     }
195     return list;
196 }
197
198 static ResourceValue literalIMAccount(const QString &accountPath)
199 {
200     return ResourceValue(imAccount(accountPath));
201 }
202
203 static ResourceValue literalIMAccount(const CDTpAccountPtr &accountWrapper)
204 {
205     return ResourceValue(imAccount(accountWrapper->account()->objectPath()));
206 }
207
208 static ValueList literalIMAccountList(const QList<CDTpAccountPtr> &accounts)
209 {
210     ValueList list;
211     Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
212         list.addValue(LiteralValue(imAccount(accountWrapper->account()->objectPath())));
213     }
214     return list;
215 }
216
217 static LiteralValue literalContactInfo(const Tp::ContactInfoField &field, int i)
218 {
219     if (i >= field.fieldValue.count()) {
220         return LiteralValue(QLatin1String(""));
221     }
222
223     return LiteralValue(field.fieldValue[i]);
224 }
225
226 static void addPresence(PatternGroup &g,
227         const Value &imAddress,
228         const Tp::Presence &presence)
229 {
230     g.addPattern(imAddress, nco::imPresence::resource(), presenceType(presence.type()));
231     g.addPattern(imAddress, nco::presenceLastModified::resource(), literalTimeStamp());
232     g.addPattern(imAddress, nco::imStatusMessage::resource(), LiteralValue(presence.statusMessage()));
233 }
234
235 static void addCapabilities(PatternGroup &g,
236         const Value &imAddress,
237         Tp::CapabilitiesBase capabilities)
238 {
239     /* FIXME: We could also add im_capability_stream_tubes and
240      * im_capability_dbus_tubes */
241
242     if (capabilities.textChats()) {
243         g.addPattern(imAddress, nco::imCapability::resource(), nco::im_capability_text_chat::resource());
244     }
245     if (capabilities.streamedMediaCalls()) {
246         g.addPattern(imAddress, nco::imCapability::resource(), nco::im_capability_media_calls::resource());
247     }
248     if (capabilities.streamedMediaAudioCalls()) {
249         g.addPattern(imAddress, nco::imCapability::resource(), nco::im_capability_audio_calls::resource());
250     }
251     if (capabilities.streamedMediaVideoCalls()) {
252         g.addPattern(imAddress, nco::imCapability::resource(), nco::im_capability_video_calls::resource());
253     }
254     if (capabilities.upgradingStreamedMediaCalls()) {
255         g.addPattern(imAddress, nco::imCapability::resource(), nco::im_capability_upgrading_calls::resource());
256     }
257     if (capabilities.fileTransfers()) {
258         g.addPattern(imAddress, nco::imCapability::resource(), nco::im_capability_file_transfers::resource());
259     }
260 }
261
262 static void addAvatar(PatternGroup &g,
263         const Value &imAddress,
264         const QString &fileName,
265         bool knownAvatar)
266 {
267     if (!fileName.isEmpty()) {
268         const QUrl url = QUrl::fromLocalFile(fileName);
269         const Value dataObject = ResourceValue(url);
270         g.addPattern(dataObject, aValue, nfo::FileDataObject::resource());
271         g.addPattern(dataObject, nie::url::resource(), LiteralValue(url));
272         g.addPattern(imAddress, nco::imAvatar::resource(), dataObject);
273     } else if (knownAvatar) {
274         /* We know that the contact has no avatar, that is different to having
275          * unknown avatar which can happen for offline contacts and in which
276          * case we want to keep the latest known avatar.
277          *
278          * This is a hack to profit of IoR speedup: we set imAvatar to an IRI
279          * that does not exists, easier/faster than deleting the property in a
280          * separate query. Qct will deal with that. */
281         const static ResourceValue noAvatar(QLatin1String("urn:contactsd:invalid-avatar"));
282         g.addPattern(imAddress, nco::imAvatar::resource(), noAvatar);
283     }
284 }
285
286 static Value ensureContactAffiliation(PatternGroup &g,
287         QHash<QString, Value> &affiliations,
288         QString affiliationLabel,
289         const Value &imContact)
290 {
291     if (!affiliations.contains(affiliationLabel)) {
292         const BlankValue affiliation;
293         g.addPattern(affiliation, aValue, nco::Affiliation::resource());
294         g.addPattern(affiliation, rdfs::label::resource(), LiteralValue(affiliationLabel));
295         g.addPattern(imContact, nco::hasAffiliation::resource(), affiliation);
296         affiliations.insert(affiliationLabel, affiliation);
297     }
298
299     return affiliations[affiliationLabel];
300 }
301
302 static CDTpQueryBuilder createContactInfoBuilder(CDTpContactPtr contactWrapper)
303 {
304     if (!contactWrapper->isInformationKnown()) {
305         debug() << "contact information is unknown";
306         return CDTpQueryBuilder();
307     }
308
309     Tp::ContactInfoFieldList listContactInfo = contactWrapper->contact()->infoFields().allFields();
310     if (listContactInfo.count() == 0) {
311         debug() << "contact information is empty";
312         return CDTpQueryBuilder();
313     }
314
315     /* Create a builder with ?imContact bound to this contact.
316      * Use the imAddress as graph for ContactInfo fields, so we can easilly
317      * know from which contact it comes from */
318     CDTpQueryBuilder builder;
319     Insert i(Insert::Replace);
320     Graph g(literalIMAddress(contactWrapper));
321     i.addRestriction(imContactVar, imAddressChain, literalIMAddress(contactWrapper));
322
323     QHash<QString, Value> affiliations;
324     Q_FOREACH (const Tp::ContactInfoField &field, listContactInfo) {
325         if (field.fieldValue.count() == 0) {
326             continue;
327         }
328
329         /* Extract field types */
330         QStringList subTypes;
331         QString affiliationLabel = QLatin1String("Other");
332         Q_FOREACH (const QString &param, field.parameters) {
333             if (!param.startsWith(QLatin1String("type="))) {
334                 continue;
335             }
336             const QString type = param.mid(5);
337             if (type == QLatin1String("home")) {
338                 affiliationLabel = QLatin1String("Home");
339             } else if (type == QLatin1String("work")) {
340                 affiliationLabel = QLatin1String("Work");
341             } else if (!subTypes.contains(type)){
342                 subTypes << type;
343             }
344         }
345
346         /* FIXME: do we care about "fn" and "nickname" ? */
347         if (!field.fieldName.compare(QLatin1String("tel"))) {
348             static QHash<QString, Value> knownTypes;
349             if (knownTypes.isEmpty()) {
350                 knownTypes.insert(QLatin1String("bbsl"), nco::BbsNumber::resource());
351                 knownTypes.insert(QLatin1String("car"), nco::CarPhoneNumber::resource());
352                 knownTypes.insert(QLatin1String("cell"), nco::CellPhoneNumber::resource());
353                 knownTypes.insert(QLatin1String("fax"), nco::FaxNumber::resource());
354                 knownTypes.insert(QLatin1String("isdn"), nco::IsdnNumber::resource());
355                 knownTypes.insert(QLatin1String("modem"), nco::ModemNumber::resource());
356                 knownTypes.insert(QLatin1String("pager"), nco::PagerNumber::resource());
357                 knownTypes.insert(QLatin1String("pcs"), nco::PcsNumber::resource());
358                 knownTypes.insert(QLatin1String("video"), nco::VideoTelephoneNumber::resource());
359                 knownTypes.insert(QLatin1String("voice"), nco::VoicePhoneNumber::resource());
360             }
361
362             QStringList realSubTypes;
363             Q_FOREACH (const QString &type, subTypes) {
364                 if (knownTypes.contains(type)) {
365                     realSubTypes << type;
366                 }
367             }
368
369             const Value affiliation = ensureContactAffiliation(g, affiliations, affiliationLabel, imContactVar);
370             const Value phoneNumber = qctMakePhoneNumberResource(field.fieldValue[0], realSubTypes);
371             g.addPattern(phoneNumber, aValue, nco::PhoneNumber::resource());
372             Q_FOREACH (const QString &type, realSubTypes) {
373                 g.addPattern(phoneNumber, aValue, knownTypes[type]);
374             }
375             g.addPattern(phoneNumber, nco::phoneNumber::resource(), literalContactInfo(field, 0));
376             g.addPattern(phoneNumber, maemo::localPhoneNumber::resource(),
377                     LiteralValue(qctMakeLocalPhoneNumber(field.fieldValue[0])));
378             g.addPattern(affiliation, nco::hasPhoneNumber::resource(), phoneNumber);
379         }
380
381         else if (!field.fieldName.compare(QLatin1String("adr"))) {
382             static QHash<QString, Value> knownTypes;
383             if (knownTypes.isEmpty()) {
384                 knownTypes.insert(QLatin1String("dom"), nco::DomesticDeliveryAddress::resource());
385                 knownTypes.insert(QLatin1String("intl"), nco::InternationalDeliveryAddress::resource());
386                 knownTypes.insert(QLatin1String("parcel"), nco::ParcelDeliveryAddress::resource());
387                 knownTypes.insert(QLatin1String("postal"), maemo::PostalAddress::resource());
388             }
389
390             QStringList realSubTypes;
391             Q_FOREACH (const QString &type, subTypes) {
392                 if (knownTypes.contains(type)) {
393                     realSubTypes << type;
394                 }
395             }
396
397             const Value affiliation = ensureContactAffiliation(g, affiliations, affiliationLabel, imContactVar);
398             const BlankValue postalAddress;
399             g.addPattern(postalAddress, aValue, nco::PostalAddress::resource());
400             Q_FOREACH (const QString &type, realSubTypes) {
401                 g.addPattern(postalAddress, aValue, knownTypes[type]);
402             }
403             g.addPattern(postalAddress, nco::pobox::resource(),           literalContactInfo(field, 0));
404             g.addPattern(postalAddress, nco::extendedAddress::resource(), literalContactInfo(field, 1));
405             g.addPattern(postalAddress, nco::streetAddress::resource(),   literalContactInfo(field, 2));
406             g.addPattern(postalAddress, nco::locality::resource(),        literalContactInfo(field, 3));
407             g.addPattern(postalAddress, nco::region::resource(),          literalContactInfo(field, 4));
408             g.addPattern(postalAddress, nco::postalcode::resource(),      literalContactInfo(field, 5));
409             g.addPattern(postalAddress, nco::country::resource(),         literalContactInfo(field, 6));
410             g.addPattern(affiliation, nco::hasPostalAddress::resource(), postalAddress);
411         }
412
413         else if (!field.fieldName.compare(QLatin1String("email"))) {
414             static const QString tmpl = QString::fromLatin1("mailto:%1");
415             const Value emailAddress = ResourceValue(tmpl.arg(field.fieldValue[0]));
416             const Value affiliation = ensureContactAffiliation(g, affiliations, affiliationLabel, imContactVar);
417             g.addPattern(emailAddress, aValue, nco::EmailAddress::resource());
418             g.addPattern(emailAddress, nco::emailAddress::resource(), literalContactInfo(field, 0));
419             g.addPattern(affiliation, nco::hasEmailAddress::resource(), emailAddress);
420         }
421
422         else if (!field.fieldName.compare(QLatin1String("url"))) {
423             const Value affiliation = ensureContactAffiliation(g, affiliations, affiliationLabel, imContactVar);
424             g.addPattern(affiliation, nco::url::resource(), literalContactInfo(field, 0));
425         }
426
427         else if (!field.fieldName.compare(QLatin1String("title"))) {
428             const Value affiliation = ensureContactAffiliation(g, affiliations, affiliationLabel, imContactVar);
429             g.addPattern(affiliation, nco::title::resource(), literalContactInfo(field, 0));
430         }
431
432         else if (!field.fieldName.compare(QLatin1String("role"))) {
433             const Value affiliation = ensureContactAffiliation(g, affiliations, affiliationLabel, imContactVar);
434             g.addPattern(affiliation, nco::role::resource(), literalContactInfo(field, 0));
435         }
436
437         else if (!field.fieldName.compare(QLatin1String("org"))) {
438             const Value affiliation = ensureContactAffiliation(g, affiliations, affiliationLabel, imContactVar);
439             const BlankValue organizationContact;
440             g.addPattern(organizationContact, aValue, nco::OrganizationContact::resource());
441             g.addPattern(organizationContact, nco::fullname::resource(), literalContactInfo(field, 0));
442             g.addPattern(affiliation, nco::department::resource(), literalContactInfo(field, 1));
443             g.addPattern(affiliation, nco::org::resource(), organizationContact);
444         }
445
446         else if (!field.fieldName.compare(QLatin1String("note")) || !field.fieldName.compare(QLatin1String("desc"))) {
447             g.addPattern(imContactVar, nco::note::resource(), literalContactInfo(field, 0));
448         }
449
450         else if (!field.fieldName.compare(QLatin1String("bday"))) {
451             /* Tracker will reject anything not [-]CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm]
452              * VCard spec allows only ISO 8601, but most IM clients allows
453              * any string. */
454             /* FIXME: support more date format for compatibility */
455             QDate date = QDate::fromString(field.fieldValue[0], QLatin1String("yyyy-MM-dd"));
456             if (!date.isValid()) {
457                 date = QDate::fromString(field.fieldValue[0], QLatin1String("yyyyMMdd"));
458             }
459
460             if (date.isValid()) {
461                 g.addPattern(imContactVar, nco::birthDate::resource(), LiteralValue(QDateTime(date)));
462             } else {
463                 debug() << "Unsupported bday format:" << field.fieldValue[0];
464             }
465         }
466
467         else if (!field.fieldName.compare(QLatin1String("x-gender"))) {
468             if (field.fieldValue[0] == QLatin1String("male")) {
469                 g.addPattern(imContactVar, nco::gender::resource(), nco::gender_male::resource());
470             } else if (field.fieldValue[0] == QLatin1String("female")) {
471                 g.addPattern(imContactVar, nco::gender::resource(), nco::gender_female::resource());
472             } else {
473                 debug() << "Unsupported gender:" << field.fieldValue[0];
474             }
475         }
476
477         else {
478             debug() << "Unsupported VCard field" << field.fieldName;
479         }
480     }
481     i.addData(g);
482     builder.append(i);
483
484     return builder;
485 }
486
487 static void addRemoveContactInfo(Delete &d,
488         const Variable &imAddress,
489         const Value &imContact)
490 {
491     /* Remove all triples on imContact and in graph. All sub-resources will be
492      * GCed by qct sometimes.
493      * imAddress is used as graph for properties on the imContact */
494     deletePropertyWithGraph(d, imContact, nco::birthDate::resource(), imAddress);
495     deletePropertyWithGraph(d, imContact, nco::gender::resource(), imAddress);
496     deletePropertyWithGraph(d, imContact, nco::note::resource(), imAddress);
497     deletePropertyWithGraph(d, imContact, nco::hasAffiliation::resource(), imAddress);
498 }
499
500 static QString saveAccountAvatar(CDTpAccountPtr accountWrapper)
501 {
502     const Tp::Avatar &avatar = accountWrapper->account()->avatar();
503
504     if (avatar.avatarData.isEmpty()) {
505         return QString();
506     }
507
508     static const QString tmpl = QString::fromLatin1("%1/.contacts/avatars/%2");
509     QString fileName = tmpl.arg(QDir::homePath())
510         .arg(QLatin1String(QCryptographicHash::hash(avatar.avatarData, QCryptographicHash::Sha1).toHex()));
511     debug() << "Saving account avatar to" << fileName;
512
513     QFile avatarFile(fileName);
514     if (!avatarFile.open(QIODevice::WriteOnly)) {
515         warning() << "Unable to save account avatar: error opening avatar "
516             "file" << fileName << "for writing";
517         return QString();
518     }
519     avatarFile.write(avatar.avatarData);
520     avatarFile.close();
521
522     return fileName;
523 }
524
525 static void addAccountChanges(PatternGroup &g,
526         CDTpAccountPtr accountWrapper,
527         CDTpAccount::Changes changes)
528 {
529     Tp::AccountPtr account = accountWrapper->account();
530     const Value imAccount = literalIMAccount(accountWrapper);
531     const Value imAddress = literalIMAddress(accountWrapper);
532
533     if (changes & CDTpAccount::Presence) {
534         debug() << "  presence changed";
535         addPresence(g, imAddress, account->currentPresence());
536     }
537     if (changes & CDTpAccount::Avatar) {
538         debug() << "  avatar changed";
539         addAvatar(g, imAddress, saveAccountAvatar(accountWrapper), true);
540     }
541     if (changes & CDTpAccount::Nickname) {
542         debug() << "  nickname changed";
543         g.addPattern(imAddress, nco::imNickname::resource(), LiteralValue(account->nickname()));
544     }
545     if (changes & CDTpAccount::DisplayName) {
546         debug() << "  display name changed";
547         g.addPattern(imAccount, nco::imDisplayName::resource(), LiteralValue(account->displayName()));
548     }
549     if (changes & CDTpAccount::Enabled) {
550         debug() << "  enabled changed";
551         g.addPattern(imAccount, nco::imEnabled::resource(), LiteralValue(account->isEnabled()));
552     }
553 }
554
555 static void addContactChanges(PatternGroup &g,
556         const Value &imAddress,
557         CDTpContact::Changes changes,
558         Tp::ContactPtr contact)
559 {
560     debug() << "Update contact" << imAddress.sparql();
561
562     // Apply changes
563     if (changes & CDTpContact::Alias) {
564         debug() << "  alias changed";
565         g.addPattern(imAddress, nco::imNickname::resource(), LiteralValue(contact->alias().trimmed()));
566     }
567     if (changes & CDTpContact::Presence) {
568         debug() << "  presence changed";
569         addPresence(g, imAddress, contact->presence());
570     }
571     if (changes & CDTpContact::Capabilities) {
572         debug() << "  capabilities changed";
573         addCapabilities(g, imAddress, contact->capabilities());
574     }
575     if (changes & CDTpContact::Avatar) {
576         debug() << "  avatar changed";
577         addAvatar(g, imAddress, contact->avatarData().fileName, contact->isAvatarTokenKnown());
578     }
579     if (changes & CDTpContact::Authorization) {
580         debug() << "  authorization changed";
581         g.addPattern(imAddress, nco::imAddressAuthStatusFrom::resource(),
582                 presenceState(contact->subscriptionState()));
583         g.addPattern(imAddress, nco::imAddressAuthStatusTo::resource(),
584                 presenceState(contact->publishState()));
585     }
586 }
587
588 static CDTpQueryBuilder createAccountsBuilder(const QList<CDTpAccountPtr> &accounts)
589 {
590     CDTpQueryBuilder builder;
591     Insert i(Insert::Replace);
592
593     Graph g(defaultGraph);
594     g.addPattern(nco::default_contact_me::resource(), nie::contentLastModified::resource(), literalTimeStamp());
595     i.addData(g);
596
597     g = Graph(privateGraph);
598     Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
599         Tp::AccountPtr account = accountWrapper->account();
600         const Value imAccount = literalIMAccount(accountWrapper);
601         const Value imAddress = literalIMAddress(accountWrapper);
602
603         debug() << "Create account" << imAddress.sparql();
604
605         // Ensure the IMAccount exists
606         g.addPattern(imAccount, aValue, nco::IMAccount::resource());
607         g.addPattern(imAccount, nco::imAccountType::resource(), LiteralValue(account->protocolName()));
608         g.addPattern(imAccount, nco::imAccountAddress::resource(), imAddress);
609         g.addPattern(imAccount, nco::hasIMContact::resource(), imAddress);
610
611         // Ensure the self contact has an IMAddress
612         g.addPattern(imAddress, aValue, nco::IMAddress::resource());
613         g.addPattern(imAddress, nco::imID::resource(), LiteralValue(account->normalizedName()));
614         g.addPattern(imAddress, nco::imProtocol::resource(), LiteralValue(account->protocolName()));
615
616         // Add all mutable properties
617         addAccountChanges(g, accountWrapper, CDTpAccount::All);
618     }
619     i.addData(g);
620     builder.append(i);
621
622     // Ensure the IMAddresses are bound to the default-contact-me via an affiliation
623     const Value affiliation = BlankValue(QString::fromLatin1("affiliation"));
624     Exists e;
625     i = Insert(Insert::Replace);
626     g = Graph(privateGraph);
627     g.addPattern(nco::default_contact_me::resource(), nco::hasAffiliation::resource(), affiliation);
628     g.addPattern(affiliation, aValue, nco::Affiliation::resource());
629     g.addPattern(affiliation, nco::hasIMAddress::resource(), imAddressVar);
630     g.addPattern(affiliation, rdfs::label::resource(), LiteralValue(QString::fromLatin1("Other")));
631     i.addData(g);
632     i.addRestriction(imAddressVar, aValue, nco::IMAddress::resource());
633     e.addPattern(nco::default_contact_me::resource(), imAddressChain, imAddressVar);
634     i.setFilter(Filter(Functions::and_.apply(
635             Functions::in.apply(Functions::str.apply(imAddressVar), literalIMAddressList(accounts)),
636             Functions::not_.apply(Filter(e)))));
637     builder.append(i);
638
639     return builder;
640 }
641
642 static CDTpQueryBuilder createContactsBuilder(const QList<CDTpContactPtr> &contacts)
643 {
644     CDTpQueryBuilder builder;
645
646     // Ensure all imAddress exists and are linked from imAccount
647     Insert i(Insert::Replace);
648     Graph g(privateGraph);
649     Q_FOREACH (const CDTpContactPtr contactWrapper, contacts) {
650         CDTpAccountPtr accountWrapper = contactWrapper->accountWrapper();
651
652         const Value imAddress = literalIMAddress(contactWrapper);
653         g.addPattern(imAddress, aValue, nco::IMAddress::resource());
654         g.addPattern(imAddress, nco::imID::resource(), LiteralValue(contactWrapper->contact()->id()));
655         g.addPattern(imAddress, nco::imProtocol::resource(), LiteralValue(accountWrapper->account()->protocolName()));
656
657         const Value imAccount = literalIMAccount(accountWrapper);
658         g.addPattern(imAccount, nco::hasIMContact::resource(), imAddress);
659
660         // Add mutable properties except for ContactInfo
661         addContactChanges(g, imAddress, CDTpContact::All, contactWrapper->contact());
662     }
663     i.addData(g);
664     builder.append(i);
665
666     // Ensure all imAddresses are bound to a PersonContact
667     i = Insert();
668     g = Graph(defaultGraph);
669     BlankValue imContact(QString::fromLatin1("contact"));
670     g.addPattern(imContact, aValue, nco::PersonContact::resource());
671     g.addPattern(imContact, nie::contentCreated::resource(), literalTimeStamp());
672     g.addPattern(imContact, nie::contentLastModified::resource(), literalTimeStamp());
673     g.addPattern(imContact, nie::generator::resource(), defaultGenerator);
674     i.addData(g);
675     g = Graph(privateGraph);
676     BlankValue affiliation(QString::fromLatin1("affiliation"));
677     g.addPattern(imContact, nco::hasAffiliation::resource(), affiliation);
678     g.addPattern(affiliation, aValue, nco::Affiliation::resource());
679     g.addPattern(affiliation, nco::hasIMAddress::resource(), imAddressVar);
680     g.addPattern(affiliation, rdfs::label::resource(), LiteralValue(QString::fromLatin1("Other")));
681     i.addData(g);
682     g = Graph(privateGraph);
683     Variable imId;
684     g.addPattern(imAddressVar, nco::imID::resource(), imId);
685     i.addRestriction(g);
686     Exists e;
687     e.addPattern(imContactVar, imAddressChain, imAddressVar);
688     i.setFilter(Functions::not_.apply(Filter(e)));
689     builder.append(i);
690
691     // Add ContactInfo seperately because we need to bind to the PersonContact
692     Q_FOREACH (const CDTpContactPtr contactWrapper, contacts) {
693         builder.append(createContactInfoBuilder(contactWrapper));
694     }
695
696     return builder;
697 }
698
699 static CDTpQueryBuilder purgeContactsBuilder()
700 {
701     static const Function equalsMeContact = Functions::equal.apply(imContactVar,
702                                                                    nco::default_contact_me::resource());
703     /* Purge nco:IMAddress not bound from an nco:IMAccount */
704
705     CDTpQueryBuilder builder;
706
707     /* Step 1 - Clean nco:PersonContact from all info imported from the imAddress.
708      * Note: We don't delete the affiliation because it could contain other
709      * info (See NB#239973) */
710     Delete d;
711     Variable affiliationVar;
712     d.addData(affiliationVar, nco::hasIMAddress::resource(), imAddressVar);
713     Graph g(privateGraph);
714     Variable imId;
715     g.addPattern(imAddressVar, nco::imID::resource(), imId);
716     d.addRestriction(g);
717     d.addRestriction(imContactVar, nco::hasAffiliation::resource(), affiliationVar);
718     d.addRestriction(affiliationVar, nco::hasIMAddress::resource(), imAddressVar);
719     Exists e;
720     e.addPattern(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
721     Exists e2;
722     e2.addPattern(imAddressVar, nco::imAddressAuthStatusFrom::resource(), Variable());
723     /* we delete addresses that are not connected to an IMAccount, but spare the ones
724        that don't have their AuthStatus set (those are failed invitations)
725        The IMAddress on me-contact has no AuthStatus set, but we delete it anyway*/
726     d.setFilter(Filter(Functions::and_.apply(Functions::not_.apply(Filter(e)),
727                                              Functions::or_.apply(Filter(e2),
728                                                                   equalsMeContact))));
729     deleteProperty(d, imContactVar, nie::contentLastModified::resource());
730     addRemoveContactInfo(d, imAddressVar, imContactVar);
731     builder.append(d);
732
733     /* Step 1.1 - Remove the nco:IMAddress resource.
734      * This must be done in a separate query because of NB#242979 */
735     d = Delete();
736     d.addData(imAddressVar, aValue, rdfs::Resource::resource());
737     g = Graph(privateGraph);
738     imId = Variable();
739     g.addPattern(imAddressVar, nco::imID::resource(), imId);
740     d.addRestriction(g);
741     e = Exists();
742     e.addPattern(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
743     d.setFilter(Filter(Functions::not_.apply(Filter(e))));
744     builder.append(d);
745
746     /* Step 2 - Purge nco:PersonContact with generator "telepathy" but with no
747      * nco:IMAddress bound anymore */
748     d = Delete();
749     d.addData(imContactVar, aValue, rdfs::Resource::resource());
750     d.addRestriction(imContactVar, nie::generator::resource(), defaultGenerator);
751     e = Exists();
752     Variable v;
753     e.addPattern(imContactVar, imAddressChain, v);
754     d.setFilter(Filter(Functions::not_.apply(Filter(e))));
755     builder.append(d);
756
757     /* Step 3 - Add back nie:contentLastModified for nco:PersonContact missing one */
758     Insert i(Insert::Replace);
759     g = Graph(defaultGraph);
760     g.addPattern(imContactVar, nie::contentLastModified::resource(), literalTimeStamp());
761     i.addData(g);
762     i.addRestriction(imContactVar, aValue, nco::PersonContact::resource());
763     e = Exists();
764     v = Variable();
765     e.addPattern(imContactVar, nie::contentLastModified::resource(), v);
766     i.setFilter(Filter(Functions::not_.apply(Filter(e))));
767     builder.append(i);
768
769     return builder;
770 }
771
772 static Tp::Presence unknownPresence()
773 {
774     static const Tp::Presence unknownPresence(Tp::ConnectionPresenceTypeUnknown, QLatin1String("unknown"), QString());
775     return unknownPresence;
776 }
777
778 static CDTpQueryBuilder syncNoRosterAccountsContactsBuilder(const QList<CDTpAccountPtr> accounts)
779 {
780     CDTpQueryBuilder builder;
781
782     // Set presence to UNKNOWN for all contacts, except for self contact because
783     // its presence is OFFLINE and will be set in updateAccount()
784     Insert i(Insert::Replace);
785     Graph g(privateGraph);
786     addPresence(g, imAddressVar, unknownPresence());
787     i.addData(g);
788     g = Graph(defaultGraph);
789     g.addPattern(imContactVar, nie::contentLastModified::resource(), literalTimeStamp());
790     i.addData(g);
791     i.addRestriction(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
792     i.addRestriction(imContactVar, imAddressChain, imAddressVar);
793     i.setFilter(Filter(Functions::and_.apply(
794             Functions::notIn.apply(Functions::str.apply(imAddressVar), literalIMAddressList(accounts)),
795             Functions::in.apply(Functions::str.apply(imAccountVar), literalIMAccountList(accounts)))));
796     builder.append(i);
797
798     // Remove capabilities from all imAddresses
799     Delete d;
800     d.addRestriction(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
801     d.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAccountVar), literalIMAccountList(accounts))));
802     deleteProperty(d, imAddressVar, nco::imCapability::resource());
803     builder.append(d);
804
805     // Add capabilities on all contacts for each account
806     Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
807         Insert i;
808         Graph g(privateGraph);
809         addCapabilities(g, imAddressVar, accountWrapper->account()->capabilities());
810         i.addData(g);
811         i.addRestriction(literalIMAccount(accountWrapper), nco::hasIMContact::resource(), imAddressVar);
812         builder.append(i);
813     }
814
815     return builder;
816 }
817
818 static CDTpQueryBuilder syncRosterAccountsContactsBuilder(const QList<CDTpAccountPtr> &accounts,
819         bool purgeContacts = false)
820 {
821     QList<CDTpContactPtr> allContacts;
822     Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
823         allContacts << accountWrapper->contacts();
824     }
825
826     /* Delete the hasIMContact property on IMAccounts (except for self contact)
827      * then sync all contacts (that will add back hasIMContact for them). After
828      * that we can purge all imAddresses not bound to an IMAccount anymore.
829      *
830      * Also remove imCapability from imAddresses because it is multi-valued */
831     CDTpQueryBuilder builder;
832     Delete d;
833     d.addData(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
834     d.addRestriction(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
835     d.setFilter(Filter(Functions::and_.apply(
836             Functions::notIn.apply(Functions::str.apply(imAddressVar), literalIMAddressList(accounts)),
837             Functions::in.apply(Functions::str.apply(imAccountVar), literalIMAccountList(accounts)))));
838     deleteProperty(d, imAddressVar, nco::imCapability::resource());
839     builder.append(d);
840
841     /* Delete ContactInfo for those who know it */
842     QList<CDTpContactPtr> infoContacts;
843     Q_FOREACH (const CDTpContactPtr contactWrapper, allContacts) {
844         if (contactWrapper->isInformationKnown()) {
845             infoContacts << contactWrapper;
846         }
847     }
848     if (!infoContacts.isEmpty()) {
849         Delete d;
850         d.addRestriction(imContactVar, imAddressChain, imAddressVar);
851         d.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAddressVar), literalIMAddressList(infoContacts))));
852         addRemoveContactInfo(d, imAddressVar, imContactVar);
853         builder.append(d);
854     }
855
856     /* Now create all contacts */
857     if (!allContacts.isEmpty()) {
858         builder.append(createContactsBuilder(allContacts));
859
860         /* Update timestamp on all nco:PersonContact bound to this account */
861         Insert i(Insert::Replace);
862         Graph g(defaultGraph);
863         g.addPattern(imContactVar, nie::contentLastModified::resource(), literalTimeStamp());
864         i.addData(g);
865         i.addRestriction(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
866         i.addRestriction(imContactVar, imAddressChain, imAddressVar);
867         i.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAccountVar), literalIMAccountList(accounts))));
868         builder.append(i);
869     }
870
871     /* Purge IMAddresses not bound from an account anymore */
872     if (purgeContacts) {
873         builder.append(purgeContactsBuilder());
874     }
875
876     return builder;
877 }
878
879 static CDTpQueryBuilder syncDisabledAccountsContactsBuilder(const QList<CDTpAccountPtr> &accounts)
880 {
881     CDTpQueryBuilder builder;
882
883     /* Disabled account: We want remove pure-IM contacts, but for merged/edited
884      * contacts we want to keep only its IMAddress as local information, so it
885      * will be merged again when we re-enable the account. */
886
887     /* Step 1 - Unlink pure-IM IMAddress from its IMAccount */
888     Delete d;
889     d.addData(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
890     d.addRestriction(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
891     d.addRestriction(imContactVar, imAddressChain, imAddressVar);
892     d.addRestriction(imContactVar, nie::generator::resource(), defaultGenerator);
893     d.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAccountVar), literalIMAccountList(accounts))));
894     builder.append(d);
895
896     /* Step 2 - Delete pure-IM contacts since they are now unbound */
897     builder.append(purgeContactsBuilder());
898
899     /* Step 3 - remove all imported info from merged/edited contacts (those remaining) */
900     d = Delete();
901     d.addRestriction(imContactVar, imAddressChain, imAddressVar);
902     d.addRestriction(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
903     addRemoveContactInfo(d, imAddressVar, imContactVar);
904     deleteProperty(d, imAddressVar, nco::imPresence::resource());
905     deleteProperty(d, imAddressVar, nco::presenceLastModified::resource());
906     deleteProperty(d, imAddressVar, nco::imStatusMessage::resource());
907     deleteProperty(d, imAddressVar, nco::imCapability::resource());
908     deleteProperty(d, imAddressVar, nco::imAvatar::resource());
909     deleteProperty(d, imAddressVar, nco::imNickname::resource());
910     deleteProperty(d, imAddressVar, nco::imAddressAuthStatusFrom::resource());
911     deleteProperty(d, imAddressVar, nco::imAddressAuthStatusTo::resource());
912     d.setFilter(Filter(Functions::and_.apply(
913         Functions::in.apply(Functions::str.apply(imAccountVar), literalIMAccountList(accounts)),
914         Functions::notIn.apply(Functions::str.apply(imAddressVar), literalIMAddressList(accounts)))));
915     builder.append(d);
916
917     /* Step 4 - move merged/edited IMAddress to qct's graph and update timestamp */
918     Insert i(Insert::Replace);
919     Graph g(defaultGraph);
920     Variable imId;
921     g.addPattern(imContactVar, nie::contentLastModified::resource(), literalTimeStamp());
922     g.addPattern(imAddressVar, nco::imID::resource(), imId);
923     i.addData(g);
924     g = Graph(privateGraph);
925     g.addPattern(imAddressVar, nco::imID::resource(), imId);
926     i.addRestriction(g);
927     i.addRestriction(imAccountVar, nco::hasIMContact::resource(), imAddressVar);
928     i.addRestriction(imContactVar, imAddressChain, imAddressVar);
929     i.setFilter(Filter(Functions::and_.apply(
930         Functions::in.apply(Functions::str.apply(imAccountVar), literalIMAccountList(accounts)),
931         Functions::notIn.apply(Functions::str.apply(imAddressVar), literalIMAddressList(accounts)))));
932     builder.append(i);
933
934     return builder;
935 }
936
937 static CDTpQueryBuilder removeContactsBuilder(const QString &accountPath,
938         const QStringList &contactIds)
939 {
940     // delete all nco:hasIMContact link from the nco:IMAccount
941     CDTpQueryBuilder builder;
942     Delete d;
943     const Value imAccount = literalIMAccount(accountPath);
944     Q_FOREACH (const QString &contactId, contactIds) {
945         d.addData(imAccount, nco::hasIMContact::resource(), literalIMAddress(accountPath, contactId));
946     }
947     builder.append(d);
948
949     // Now purge contacts not linked from an IMAccount
950     builder.append(purgeContactsBuilder());
951
952     return builder;
953 }
954
955 static CDTpQueryBuilder removeContactsBuilder(CDTpAccountPtr accountWrapper,
956         const QList<CDTpContactPtr> &contacts)
957 {
958     QStringList contactIds;
959     Q_FOREACH (const CDTpContactPtr &contactWrapper, contacts) {
960         contactIds << contactWrapper->contact()->id();
961     }
962
963     return removeContactsBuilder(accountWrapper->account()->objectPath(), contactIds);
964 }
965
966 static CDTpQueryBuilder createIMAddressBuilder(const QString &accountPath,
967                                                const QStringList &imIds,
968                                                uint localId)
969 {
970     CDTpQueryBuilder builder;
971
972     Insert i(Insert::Replace);
973     Graph g(privateGraph);
974     const ResourceValue imAccount = literalIMAccount(accountPath);
975     PatternGroup restrictions;
976
977     Q_FOREACH (const QString &imId, imIds) {
978         const ResourceValue imAddress = literalIMAddress(accountPath, imId);
979         const BlankValue affiliation;
980
981         g.addPattern(imAddress, rdf::type::resource(), nco::IMAddress::resource());
982         g.addPattern(imAddress, nco::imID::resource(), LiteralValue(imId));
983         g.addPattern(affiliation, rdf::type::resource(), nco::Affiliation::resource());
984         g.addPattern(affiliation, nco::hasIMAddress::resource(), imAddress);
985         g.addPattern(imContactVar, nco::hasAffiliation::resource(), affiliation);
986         g.addPattern(imAccount, nco::hasIMContact::resource(), imAddress);
987     }
988
989     i.addData(g);
990
991     restrictions.addPattern(imContactVar, rdf::type::resource(), nco::PersonContact::resource());
992     restrictions.setFilter(Functions::equal.apply(Functions::trackerId.apply(imContactVar),
993                                                    LiteralValue(localId)));
994     i.addRestriction(restrictions);
995
996     builder.append(i);
997
998     return builder;
999 }
1000
1001 static CDTpQueryBuilder createGarbageCollectorBuilder()
1002 {
1003     CDTpQueryBuilder builder;
1004
1005     /* Resources we leak in the various queries:
1006      *
1007      *  - nfo:FileDataObject in privateGraph for each avatar update.
1008      *  - nco:Affiliation in privateGraph for each deleted IMAddress.
1009      *  - nco:Affiliationn nco:PhoneNumber, nco:PostalAddress, nco:EmailAddress
1010      *    and nco:OrganizationContact in the IMAddress graph for each
1011      *    ContactInfo update.
1012      */
1013
1014     /* Each avatar update leaks the previous nfo:FileDataObject in privateGraph */
1015     Delete d;
1016     Exists e;
1017     Graph g(privateGraph);
1018     Variable dataObject;
1019     d.addData(dataObject, aValue, nfo::FileDataObject::resource());
1020     g.addPattern(dataObject, aValue, nfo::FileDataObject::resource());
1021     e.addPattern(imAddressVar, nco::imAvatar::resource(), dataObject);
1022     d.addRestriction(g);
1023     d.setFilter(Functions::not_.apply(Filter(e)));
1024     builder.append(d);
1025
1026     /* Affiliations used to link to the IMAddress are leaked when IMAddress is
1027      * deleted. If affiliation is in privateGraph and has no hasIMAddress we
1028      * can purge it and unlink from PersonContact.
1029      *
1030      * FIXME: Because of NB#242979 we need to split the unlink and actually
1031      * deletion of the affiliation
1032      */
1033
1034     /* Part 1. Delete the affiliation */
1035     d = Delete();
1036     g = Graph(privateGraph);
1037     e = Exists();
1038     Variable affiliation;
1039     d.addData(affiliation, aValue, rdfs::Resource::resource());
1040     g.addPattern(affiliation, aValue, nco::Affiliation::resource());
1041     d.addRestriction(g);
1042     e.addPattern(affiliation, nco::hasIMAddress::resource(), Variable());
1043     d.setFilter(Functions::not_.apply(Filter(e)));
1044     builder.append(d);
1045
1046     /* Part 2. Delete hasAffiliation if linked resource does not exist anymore  */
1047     d = Delete();
1048     g = Graph(privateGraph);
1049     e = Exists();
1050     d.addData(imContactVar, nco::hasAffiliation::resource(), affiliation);
1051     g.addPattern(imContactVar, nco::hasAffiliation::resource(), affiliation);
1052     d.addRestriction(g);
1053     e.addPattern(affiliation, aValue, rdfs::Resource::resource());
1054     d.setFilter(Functions::not_.apply(Filter(e)));
1055     builder.append(d);
1056
1057     /* Each ContactInfo update leaks various resources in IMAddress graph. But
1058      * the IMAddress could even not exist anymore... so drop everything from
1059      * a telepathy: graph not linked anymore
1060      */
1061
1062     typedef QPair<Value, Value> ValuePair;
1063     static QList<ValuePair> contactInfoResources;
1064     if (contactInfoResources.isEmpty()) {
1065         contactInfoResources << ValuePair(nco::Affiliation::resource(), nco::hasAffiliation::resource());
1066         contactInfoResources << ValuePair(nco::PhoneNumber::resource(), nco::hasPhoneNumber::resource());
1067         contactInfoResources << ValuePair(nco::PostalAddress::resource(), nco::hasPostalAddress::resource());
1068         contactInfoResources << ValuePair(nco::EmailAddress::resource(), nco::hasEmailAddress::resource());
1069         contactInfoResources << ValuePair(nco::OrganizationContact::resource(), nco::org::resource());
1070     }
1071
1072     Q_FOREACH (const ValuePair &pair, contactInfoResources) {
1073         Variable graph;
1074         d = Delete();
1075         e = Exists();
1076         g = Graph(graph);
1077         Variable resource;
1078         d.addData(resource, aValue, rdfs::Resource::resource());
1079         g.addPattern(resource, aValue, pair.first);
1080         e.addPattern(Variable(), pair.second, resource);
1081         d.addRestriction(g);
1082         d.setFilter(Functions::and_.apply(
1083                 Functions::startsWith.apply(graph, LiteralValue(QLatin1String("telepathy:"))),
1084                 Functions::not_.apply(Filter(e))));
1085         builder.append(d);
1086     }
1087
1088     return builder;
1089 }
1090
1091 void CDTpStorage::triggerGarbageCollector(CDTpQueryBuilder &builder, uint nContacts)
1092 {
1093     if (mDirectGC) {
1094         builder.append(createGarbageCollectorBuilder());
1095         return;
1096     }
1097
1098     static bool registered = false;
1099     if (!registered) {
1100         registered = true;
1101         QctGarbageCollector::registerQuery(QLatin1String("com.nokia.contactsd"),
1102                 createGarbageCollectorBuilder().sparql());
1103     }
1104
1105     QctGarbageCollector::trigger(QLatin1String("com.nokia.contactsd"),
1106             ((double)nContacts)/1000);
1107 }
1108
1109 void CDTpStorage::syncAccounts(const QList<CDTpAccountPtr> &accounts)
1110 {
1111     /* Remove IMAccount that does not exist anymore */
1112     CDTpQueryBuilder builder;
1113     Delete d;
1114     d.addData(imAccountVar, aValue, rdfs::Resource::resource());
1115     d.addRestriction(imAccountVar, aValue, nco::IMAccount::resource());
1116     if (!accounts.isEmpty()) {
1117         d.setFilter(Filter(Functions::notIn.apply(Functions::str.apply(imAccountVar), literalIMAccountList(accounts))));
1118     }
1119     builder.append(d);
1120
1121     /* Sync accounts and their contacts */
1122     uint nContacts = 0;
1123     if (!accounts.isEmpty()) {
1124         builder.append(createAccountsBuilder(accounts));
1125
1126         // Split accounts that have their roster, and those who don't
1127         QList<CDTpAccountPtr> disabledAccounts;
1128         QList<CDTpAccountPtr> rosterAccounts;
1129         QList<CDTpAccountPtr> noRosterAccounts;
1130         Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
1131             if (!accountWrapper->isEnabled()) {
1132                 disabledAccounts << accountWrapper;
1133             } else if (accountWrapper->hasRoster()) {
1134                 rosterAccounts << accountWrapper;
1135             } else {
1136                 noRosterAccounts << accountWrapper;
1137             }
1138             nContacts += accountWrapper->contacts().count();
1139         }
1140
1141         // Sync contacts
1142         if (!disabledAccounts.isEmpty()) {
1143             builder.append(syncDisabledAccountsContactsBuilder(disabledAccounts));
1144         }
1145
1146         if (!rosterAccounts.isEmpty()) {
1147             builder.append(syncRosterAccountsContactsBuilder(rosterAccounts));
1148         }
1149
1150         if (!noRosterAccounts.isEmpty()) {
1151             builder.append(syncNoRosterAccountsContactsBuilder(noRosterAccounts));
1152         }
1153     }
1154
1155     /* Purge IMAddresses not bound from an account anymore, this include the
1156      * self IMAddress and the default-contact-me as well */
1157     builder.append(purgeContactsBuilder());
1158
1159     triggerGarbageCollector(builder, nContacts);
1160
1161     CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1162     connect(query,
1163             SIGNAL(finished(CDTpSparqlQuery *)),
1164             SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1165 }
1166
1167 void CDTpStorage::createAccount(CDTpAccountPtr accountWrapper)
1168 {
1169     CDTpQueryBuilder builder;
1170
1171     /* Create account */
1172     QList<CDTpAccountPtr> accounts = QList<CDTpAccountPtr>() << accountWrapper;
1173     builder.append(createAccountsBuilder(accounts));
1174
1175     /* if account has no contacts, we are done */
1176     if (accountWrapper->contacts().isEmpty()) {
1177         CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1178         connect(query,
1179                 SIGNAL(finished(CDTpSparqlQuery *)),
1180                 SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1181         return;
1182     }
1183
1184     /* Create account's contacts */
1185     builder.append(createContactsBuilder(accountWrapper->contacts()));
1186
1187     /* Update timestamp on all nco:PersonContact bound to this account */
1188     Insert i(Insert::Replace);
1189     Graph g(defaultGraph);
1190     g.addPattern(imContactVar, nie::contentLastModified::resource(), literalTimeStamp());
1191     i.addData(g);
1192     i.addRestriction(literalIMAccount(accountWrapper), nco::hasIMContact::resource(), imAddressVar);
1193     i.addRestriction(imContactVar, imAddressChain, imAddressVar);
1194     builder.append(i);
1195
1196     CDTpAccountsSparqlQuery *query = new CDTpAccountsSparqlQuery(accountWrapper, builder, this);
1197     connect(query,
1198             SIGNAL(finished(CDTpSparqlQuery *)),
1199             SLOT(onSyncOperationEnded(CDTpSparqlQuery *)));
1200 }
1201
1202 void CDTpStorage::updateAccount(CDTpAccountPtr accountWrapper,
1203         CDTpAccount::Changes changes)
1204 {
1205     debug() << "Update account" << literalIMAddress(accountWrapper).sparql();
1206
1207     CDTpQueryBuilder builder;
1208
1209     Insert i(Insert::Replace);
1210     Graph g(defaultGraph);
1211     g.addPattern(nco::default_contact_me::resource(), nie::contentLastModified::resource(), literalTimeStamp());
1212     i.addData(g);
1213
1214     g = Graph(privateGraph);
1215     addAccountChanges(g, accountWrapper, changes);
1216     i.addData(g);
1217
1218     builder.append(i);
1219
1220     /* If account got disabled, we have special threatment for its contacts */
1221     if ((changes & CDTpAccount::Enabled) != 0 && !accountWrapper->isEnabled()) {
1222         cancelQueuedUpdates(accountWrapper->contacts());
1223         QList<CDTpAccountPtr> accounts = QList<CDTpAccountPtr>() << accountWrapper;
1224         builder.append(syncDisabledAccountsContactsBuilder(accounts));
1225         triggerGarbageCollector(builder, accountWrapper->contacts().count());
1226     }
1227
1228     CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1229     connect(query,
1230             SIGNAL(finished(CDTpSparqlQuery *)),
1231             SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1232 }
1233
1234 void CDTpStorage::removeAccount(CDTpAccountPtr accountWrapper)
1235 {
1236     cancelQueuedUpdates(accountWrapper->contacts());
1237
1238     const Value imAccount = literalIMAccount(accountWrapper);
1239     debug() << "Remove account" << imAccount.sparql();
1240
1241     /* Remove account */
1242     CDTpQueryBuilder builder;
1243     Delete d;
1244     d.addData(imAccount, aValue, rdfs::Resource::resource());
1245     builder.append(d);
1246
1247     /* Purge IMAddresses not bound from an account anymore.
1248      * This will at least remove the IMAddress of the self contact and update
1249      * default-contact-me */
1250     builder.append(purgeContactsBuilder());
1251
1252     triggerGarbageCollector(builder, 200);
1253
1254     CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1255     connect(query,
1256             SIGNAL(finished(CDTpSparqlQuery *)),
1257             SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1258 }
1259
1260 // This is called when account goes online/offline
1261 void CDTpStorage::syncAccountContacts(CDTpAccountPtr accountWrapper)
1262 {
1263     CDTpQueryBuilder builder;
1264
1265     QList<CDTpAccountPtr> accounts = QList<CDTpAccountPtr>() << accountWrapper;
1266     if (accountWrapper->hasRoster()) {
1267         builder = syncRosterAccountsContactsBuilder(accounts, true);
1268         triggerGarbageCollector(builder, accountWrapper->contacts().count());
1269     } else {
1270         builder = syncNoRosterAccountsContactsBuilder(accounts);
1271     }
1272
1273     /* If it is not the first time account gets a roster, or if account has
1274      * no contacts, execute query without notify import progress */
1275     if (!accountWrapper->isNewAccount() || accountWrapper->contacts().isEmpty()) {
1276         CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1277         connect(query,
1278                 SIGNAL(finished(CDTpSparqlQuery *)),
1279                 SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1280         return;
1281     }
1282
1283     CDTpAccountsSparqlQuery *query = new CDTpAccountsSparqlQuery(accountWrapper, builder, this);
1284     connect(query,
1285             SIGNAL(finished(CDTpSparqlQuery *)),
1286             SLOT(onSyncOperationEnded(CDTpSparqlQuery *)));
1287 }
1288
1289 void CDTpStorage::syncAccountContacts(CDTpAccountPtr accountWrapper,
1290         const QList<CDTpContactPtr> &contactsAdded,
1291         const QList<CDTpContactPtr> &contactsRemoved)
1292 {
1293     CDTpQueryBuilder builder;
1294
1295     if (!contactsAdded.isEmpty()) {
1296         builder.append(createContactsBuilder(contactsAdded));
1297
1298         // Update nie:contentLastModified on all nco:PersonContact bound to contacts
1299         Insert i(Insert::Replace);
1300         Graph g(defaultGraph);
1301         g.addPattern(imContactVar, nie::contentLastModified::resource(), literalTimeStamp());
1302         i.addData(g);
1303         i.addRestriction(imContactVar, imAddressChain, imAddressVar);
1304         i.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAddressVar), literalIMAddressList(contactsAdded))));
1305         builder.append(i);
1306     }
1307
1308     if (!contactsRemoved.isEmpty()) {
1309         cancelQueuedUpdates(contactsRemoved);
1310         builder.append(removeContactsBuilder(accountWrapper, contactsRemoved));
1311         triggerGarbageCollector(builder, contactsRemoved.count());
1312     }
1313
1314     CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1315     connect(query,
1316             SIGNAL(finished(CDTpSparqlQuery *)),
1317             SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1318 }
1319
1320 void CDTpStorage::createAccountContacts(const QString &accountPath, const QStringList &imIds, uint localId)
1321 {
1322     CDTpSparqlQuery *query = new CDTpSparqlQuery(createIMAddressBuilder(accountPath, imIds, localId),
1323                                                  this);
1324     connect(query,
1325             SIGNAL(finished(CDTpSparqlQuery *)),
1326             SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1327 }
1328
1329 /* Use this only in offline mode - use syncAccountContacts in online mode */
1330 void CDTpStorage::removeAccountContacts(const QString &accountPath, const QStringList &contactIds)
1331 {
1332     CDTpQueryBuilder builder;
1333
1334     builder = removeContactsBuilder(accountPath, contactIds);
1335     triggerGarbageCollector(builder, 200);
1336
1337     CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1338     connect(query,
1339             SIGNAL(finished(CDTpSparqlQuery *)),
1340             SLOT(onSparqlQueryFinished(CDTpSparqlQuery *)));
1341 }
1342
1343 void CDTpStorage::updateContact(CDTpContactPtr contactWrapper, CDTpContact::Changes changes)
1344 {
1345     if (not mUpdateQueue.contains(contactWrapper)) {
1346         mUpdateQueue.insert(contactWrapper, changes);
1347     } else {
1348         mUpdateQueue[contactWrapper] |= changes;
1349     }
1350
1351     if (not mUpdateRunning && not mUpdateTimer.isActive()) {
1352         mUpdateTimer.start();
1353     }
1354 }
1355
1356 void CDTpStorage::onUpdateQueueTimeout()
1357 {
1358     debug() << "Update" << mUpdateQueue.count() << "contacts";
1359
1360     CDTpQueryBuilder builder;
1361     Graph g(privateGraph);
1362
1363     QList<CDTpContactPtr> allContacts;
1364     QList<CDTpContactPtr> infoContacts;
1365     QList<CDTpContactPtr> capsContacts;
1366     QHash<CDTpContactPtr, CDTpContact::Changes>::const_iterator iter;
1367     for (iter = mUpdateQueue.constBegin(); iter != mUpdateQueue.constEnd(); iter++) {
1368         CDTpContactPtr contactWrapper = iter.key();
1369         CDTpContact::Changes changes = iter.value();
1370
1371         // Skip the contact in case its account was deleted before this function
1372         // was invoked
1373         if (contactWrapper->accountWrapper().isNull()) {
1374             continue;
1375         }
1376
1377         if (!contactWrapper->isVisible()) {
1378             continue;
1379         }
1380
1381         allContacts << contactWrapper;
1382
1383         // Special case for Capabilities and Information changes
1384         if (changes & CDTpContact::Capabilities) {
1385             capsContacts << contactWrapper;
1386         }
1387         if (changes & CDTpContact::Information) {
1388             infoContacts << contactWrapper;
1389         }
1390
1391         // Add IMAddress changes
1392         addContactChanges(g, literalIMAddress(contactWrapper),
1393                 changes, contactWrapper->contact());
1394     }
1395     mUpdateQueue.clear();
1396
1397     if (allContacts.isEmpty()) {
1398         debug() << "  None needs update";
1399         return;
1400     }
1401
1402     Insert i(Insert::Replace);
1403     i.addData(g);
1404     builder.append(i);
1405
1406     // prepend delete nco:imCapability for contacts who's caps changed
1407     if (!capsContacts.isEmpty()) {
1408         Delete d;
1409         Variable caps;
1410         d.addData(imAddressVar, nco::imCapability::resource(), caps);
1411         d.addRestriction(imAddressVar, nco::imCapability::resource(), caps);
1412         d.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAddressVar), literalIMAddressList(capsContacts))));
1413         builder.prepend(d);
1414     }
1415
1416     // delete ContactInfo for contacts who's info changed
1417     if (!infoContacts.isEmpty()) {
1418         Delete d;
1419         d.addRestriction(imContactVar, imAddressChain, imAddressVar);
1420         d.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAddressVar), literalIMAddressList(infoContacts))));
1421         addRemoveContactInfo(d, imAddressVar, imContactVar);
1422         builder.append(d);
1423     }
1424
1425     // Create ContactInfo for each contact who's info changed
1426     Q_FOREACH (const CDTpContactPtr contactWrapper, infoContacts) {
1427         builder.append(createContactInfoBuilder(contactWrapper));
1428     }
1429
1430     // Update nie:contentLastModified on all nco:PersonContact bound to contacts
1431     i = Insert(Insert::Replace);
1432     g = Graph(defaultGraph);
1433     g.addPattern(imContactVar, nie::contentLastModified::resource(), literalTimeStamp());
1434     i.addData(g);
1435     i.addRestriction(imContactVar, imAddressChain, imAddressVar);
1436     i.setFilter(Filter(Functions::in.apply(Functions::str.apply(imAddressVar), literalIMAddressList(allContacts))));
1437     builder.append(i);
1438
1439     triggerGarbageCollector(builder, allContacts.count());
1440
1441     // Launch the query
1442     mUpdateRunning = true;
1443     CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
1444     connect(query,
1445             SIGNAL(finished(CDTpSparqlQuery *)),
1446             SLOT(onUpdateFinished(CDTpSparqlQuery *)));
1447 }
1448
1449 void CDTpStorage::onUpdateFinished(CDTpSparqlQuery *query)
1450 {
1451     onSparqlQueryFinished(query);
1452
1453     mUpdateRunning = false;
1454
1455     if (not mUpdateQueue.isEmpty() && not mUpdateTimer.isActive()) {
1456         mUpdateTimer.start();
1457     }
1458 }
1459
1460 void CDTpStorage::cancelQueuedUpdates(const QList<CDTpContactPtr> &contacts)
1461 {
1462     Q_FOREACH (const CDTpContactPtr &contactWrapper, contacts) {
1463         mUpdateQueue.remove(contactWrapper);
1464     }
1465 }
1466
1467 void CDTpStorage::onSyncOperationEnded(CDTpSparqlQuery *query)
1468 {
1469     onSparqlQueryFinished(query);
1470
1471     CDTpAccountsSparqlQuery *accountsQuery = qobject_cast<CDTpAccountsSparqlQuery*>(query);
1472     QList<CDTpAccountPtr> accounts = accountsQuery->accounts();
1473
1474     Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
1475         accountWrapper->emitSyncEnded(accountWrapper->contacts().count(), 0);
1476     }
1477 }
1478
1479 void CDTpStorage::onSparqlQueryFinished(CDTpSparqlQuery *query)
1480 {
1481     if (query->hasError()) {
1482         QSparqlError e = query->error();
1483         ErrorCode code = ErrorUnknown;
1484         if (e.type() == QSparqlError::BackendError && e.number() == TRACKER_SPARQL_ERROR_NO_SPACE) {
1485             code = ErrorNoSpace;
1486         }
1487         Q_EMIT error(code, e.message());
1488     }
1489 }
1490