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