Changes: Build with CUBI_EXPLICIT_CONSTRUCTORS
[qtcontacts-tracker:hasselmms-qtcontacts-tracker.git] / src / engine / contactmergeworker.cpp
1 /*********************************************************************************
2  ** This file is part of QtContacts tracker storage plugin
3  **
4  ** Copyright (c) 2010-2011 Nokia Corporation and/or its subsidiary(-ies).
5  **
6  ** Contact:  Nokia Corporation (info@qt.nokia.com)
7  **
8  ** GNU Lesser General Public License Usage
9  ** This file may be used under the terms of the GNU Lesser General Public License
10  ** version 2.1 as published by the Free Software Foundation and appearing in the
11  ** file LICENSE.LGPL included in the packaging of this file.  Please review the
12  ** following information to ensure the GNU Lesser General Public License version
13  ** 2.1 requirements will be met:
14  ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
15  **
16  ** In addition, as a special exception, Nokia gives you certain additional rights.
17  ** These rights are described in the Nokia Qt LGPL Exception version 1.1, included
18  ** in the file LGPL_EXCEPTION.txt in this package.
19  **
20  ** Other Usage
21  ** Alternatively, this file may be used in accordance with the terms and
22  ** conditions contained in a signed written agreement between you and Nokia.
23  *********************************************************************************/
24
25 #include "contactmergeworker.h"
26 #include "contactmanagerengine.h"
27
28 #include <dao/querybuilder.h>
29 #include <dao/support.h>
30 #include <lib/constants.h>
31 #include <lib/contactmergerequest.h>
32 #include <lib/customdetails.h>
33 #include <lib/sparqlresolver.h>
34
35 #include <QtSparql>
36
37 #include <cubi.h>
38
39 #include <ontologies/nco.h>
40 #include <ontologies/rdf.h>
41 #include <ontologies/rdfs.h>
42
43 CUBI_USE_NAMESPACE
44 CUBI_USE_NAMESPACE_RESOURCES
45
46 QctContactMergeWorker::QctContactMergeWorker(QContactAbstractRequest *request,
47                                              QctContactManagerEngine *engine,
48                                              QObject *parent)
49     : QctBaseWorker<QctContactMergeRequest>(engine, parent)
50     , m_mergeIds(staticCast(request)->mergeIds())
51 {
52 }
53
54 QctContactMergeWorker::~QctContactMergeWorker()
55 {
56 }
57
58 bool
59 QctContactMergeWorker::verifyRequest()
60 {
61     MergeIdMap::ConstIterator it = m_mergeIds.constBegin();
62
63     for(int i = 0; it != m_mergeIds.constEnd(); ++it, ++i) {
64         if (0 == it.key() || 0 == it.value()) {
65             m_errorMap.insert(i, QContactManager::BadArgumentError);
66             setLastError(QContactManager::BadArgumentError);
67         }
68     }
69
70     if (lastError() != QContactManager::NoError) {
71         return false;
72     }
73
74     return true;
75 }
76
77 bool
78 QctContactMergeWorker::resolveContactIds()
79     {
80
81     // Resolve needed contact IRIs.
82     const QList<uint> idsToLookup = QList<uint>()
83             << m_mergeIds.uniqueKeys()
84             << m_mergeIds.values();
85
86     QctResourceIriResolver resolver(idsToLookup);
87
88     if (not resolver.lookupAndWait()) {
89         reportError(resolver.errors(), QLatin1String("Cannot resolve local ids"));
90         return false;
91     }
92
93     for (int i = 0; i < resolver.trackerIds().count(); ++i) {
94         m_contactIris.insert(resolver.trackerIds().at(i),
95                              resolver.resourceIris().at(i));
96     }
97
98     return true;
99 }
100
101 bool
102 QctContactMergeWorker::fetchContactDetails()
103 {
104     enum {SyncTargetColumn = 0, GraphedPropertiesColumn, UngraphedPropertiesColumn};
105     enum {PropertyField = 0, CardinalityField, NFields};
106
107     static const QString queryTemplate = QLatin1String
108             ("SELECT\n"
109              "  nie:generator(<%1>)\n"
110              "  (SELECT\n"
111              "        GROUP_CONCAT(fn:concat(?p, \"%2\", tracker:coalesce(nrl:maxCardinality(?p),\"\")), \"%3\")\n"
112              "   WHERE\n"
113              "   {\n"
114              "        GRAPH ?g\n"
115              "        {\n"
116              "                <%1> ?p ?o\n"
117              "        }\n"
118              "   })\n"
119              "  (SELECT\n"
120              "        GROUP_CONCAT(fn:concat(?p, \"%2\", tracker:coalesce(nrl:maxCardinality(?p),\"\")), \"%3\")\n"
121              "   WHERE\n"
122              "   {\n"
123              "        <%1> ?p ?o\n"
124              "   })\n"
125              "{}\n");
126
127     QMap<QString, bool> singleWithGraph;
128
129     foreach(const QString &contactIri, m_contactIris) {
130         const QString queryString = queryTemplate.arg(contactIri,
131                                                       QctQueryBuilder::fieldSeparator(),
132                                                       QctQueryBuilder::listSeparator());
133         const QSparqlQuery query(queryString, QSparqlQuery::SelectStatement);
134
135         QScopedPointer<QSparqlResult> result(runQuery(query, SyncQueryOptions));
136
137         if (result.isNull()) {
138             return false; // runQuery() called reportError()
139         }
140
141         // parse result
142         QString syncTarget;
143
144         while (result->next()) {
145             QStringList graphedProperties = result->stringValue(GraphedPropertiesColumn).
146                                             split(QctQueryBuilder::listSeparator());
147
148             foreach (const QString property, graphedProperties) {
149                 QStringList values = property.split(QctQueryBuilder::fieldSeparator());
150
151                 if (values.size() != NFields) {
152                     qctWarn(QString::fromLatin1("Invalid field data: %1").arg(property));
153                     return false;
154                 }
155
156                 if (values.at(CardinalityField).toInt() != 1) {
157                     m_contactPredicatesMultiWithGraph.insertMulti(contactIri, values.at(PropertyField));
158                 } else {
159                     singleWithGraph.insert(values.at(PropertyField), true);
160                     m_contactPredicatesSingleWithGraph.insertMulti(contactIri, values.at(PropertyField));
161                 }
162             }
163
164             QStringList ungraphedProperties = result->stringValue(UngraphedPropertiesColumn).
165                                               split(QctQueryBuilder::listSeparator());
166
167             foreach (const QString property, ungraphedProperties) {
168                 QStringList values = property.split(QctQueryBuilder::fieldSeparator());
169
170                 if (values.size() != NFields) {
171                     qctWarn(QString::fromLatin1("Invalid field data: %1").arg(property));
172                     return false;
173                 }
174
175                 if (values.at(CardinalityField).toInt() != 1) {
176                     m_contactPredicatesMulti.insertMulti(contactIri, values.at(PropertyField));
177                 } else {
178                     if (not singleWithGraph.contains(values.at(PropertyField))) {
179                         m_contactPredicatesSingle.insertMulti(contactIri, values.at(PropertyField));
180                     }
181                 }
182
183                 if (syncTarget.isNull()) {
184                     syncTarget = result->stringValue(SyncTargetColumn);
185                 }
186             }
187
188             if (not syncTarget.isEmpty()) {
189                 m_contactSyncTargets.insert(contactIri, syncTarget);
190             }
191         }
192     }
193
194     return true;
195 }
196
197 bool
198 QctContactMergeWorker::doMerge()
199 {
200     QString queryString;
201
202     foreach(QContactLocalId id, m_mergeIds.uniqueKeys()) {
203         queryString += buildMergeQuery(id);
204
205         if (lastError() != QContactManager::NoError) {
206             return false;
207         }
208     }
209
210     const QSparqlQuery query(queryString, QSparqlQuery::InsertStatement);
211     return not QScopedPointer<QSparqlResult>(runQuery(query, SyncQueryOptions)).isNull();
212 }
213
214 QString
215 QctContactMergeWorker::pickPreferredSyncTarget(const QSet<QString> &syncTargets,
216                                                const QString &destinationSyncTarget)
217 {
218     // Compute the sync target for the resulting contact.
219     //
220     // The choice of the sync target follows those rules:
221     //
222     //      "mfe"       + *             = "mfe"
223     //      "telepathy" + "addressbook" = "addressbook"
224     //      "telepathy" + "telepathy"   = "telepathy"
225     //      "telepathy" + "other"       = "other"
226     //
227     // First walk all the sync targets looking for an MFE sync target.
228     // If we have more than one MFE sync target, it's an error (would delete some
229     // contacts from the server)
230     // The MFE sync targets are of the form mfe#accountId
231     {
232         const QLatin1String mfePrefix = QContactSyncTarget__SyncTargetMfe;
233         QString finalSyncTarget;
234
235         foreach (const QString &syncTarget, syncTargets) {
236             if (not syncTarget.startsWith(mfePrefix, Qt::CaseInsensitive)) {
237                 continue;
238             }
239
240             if (finalSyncTarget.isEmpty()) {
241                 finalSyncTarget = syncTarget;
242                 continue;
243             }
244
245             // If we found more than one MFE sync target in the list
246             reportError("Trying to merge two contacts from different MFE accounts. "
247                         "This is not supported, since one of the source contacts "
248                         "would get deleted from the MFE server.",
249                         QContactManager::BadArgumentError);
250             return QString();
251         }
252
253         if (not finalSyncTarget.isEmpty()) {
254             return finalSyncTarget;
255         }
256     }
257
258     const QString engineSyncTarget = engine()->syncTarget();
259
260     foreach (const QString &syncTarget, syncTargets) {
261         if (syncTarget.compare(engineSyncTarget, Qt::CaseInsensitive) == 0) {
262             return syncTarget;
263         }
264     }
265
266     const QLatin1String telepathySyncTarget = QContactSyncTarget__SyncTargetTelepathy.operator QLatin1String();
267     const bool isDestinationSyncTargetTelepathy = (destinationSyncTarget.compare(telepathySyncTarget, Qt::CaseInsensitive) == 0);
268
269     // If we have only telepathy contacts in the merge request
270     if (syncTargets.size() == 1 && isDestinationSyncTargetTelepathy) {
271         return QContactSyncTarget__SyncTargetTelepathy;
272     }
273
274     // None of the rules above matched, we pick the destination sync target as a
275     // fallback except if it's telepathy (telepathy never "dominates" any other
276     // sync target) or if it's empty
277     if (not isDestinationSyncTargetTelepathy && not destinationSyncTarget.isEmpty()) {
278         return destinationSyncTarget;
279     }
280
281     // The destination sync target is telepathy or empty, but we have some other
282     // sync targets in the list too, so we pick one of them (not much else we can do)
283     foreach (const QString &syncTarget, syncTargets) {
284         if (syncTarget.compare(telepathySyncTarget, Qt::CaseInsensitive) != 0) {
285             return syncTarget;
286         }
287     }
288
289     // No valid sync target could be found - we fall back to the engine sync target
290     return engineSyncTarget;
291 }
292
293 QString
294 QctContactMergeWorker::buildMergeQuery(QContactLocalId id)
295 {
296     const Options::SparqlOptions sparqlOptions = engine()->updateQueryOptions();
297     const QString targetUrn = m_contactIris.value(id);
298     QStringList sourceUrns;
299     QStringList inserts;
300
301     foreach (QContactLocalId id, m_mergeIds.values(id)) {
302         sourceUrns.append(m_contactIris.value(id));
303     }
304
305     foreach (const QString &sourceUrn, sourceUrns) {
306         inserts.append(mergeContacts(targetUrn, sourceUrn));
307     }
308
309     Delete sourceDelete;
310     Variable deleteSubject;
311     PatternGroup deleteRestriction;
312     ValueList idsToDelete;
313
314     foreach (QContactLocalId id, m_mergeIds.values(id)) {
315        idsToDelete.addValue(LiteralValue(QVariant(id)));
316     }
317
318     sourceDelete.addData(deleteSubject, rdf::type::resource(), rdfs::Resource::resource());
319     deleteRestriction.addPattern(deleteSubject, rdf::type::resource(), rdfs::Resource::resource());
320     deleteRestriction.setFilter(Filter(Functions::in.apply(Functions::trackerId.apply(deleteSubject), idsToDelete)));
321     sourceDelete.addRestriction(deleteRestriction);
322
323
324     static const QString timestampUpdateTemplate = QString::fromLatin1(
325                 "DELETE {"
326                 "  <%1> nie:contentLastModified ?v"
327                 "} WHERE {"
328                 "  <%1> nie:contentLastModified ?v"
329                 "}"
330                 "INSERT {"
331                 "  GRAPH <%3> {"
332                 "    <%1> nie:contentLastModified \"%2\""
333                 "  }"
334                 "}"
335                 );
336
337     const QString currentDateTime = QDateTime::currentDateTime().toString(Qt::ISODate);
338
339     QString queryString;
340
341     foreach (const QString &insert, inserts) {
342         queryString.append(insert);
343     }
344
345     queryString.append(timestampUpdateTemplate.arg(targetUrn,
346                                                    currentDateTime,
347                                                    QtContactsTrackerDefaultGraphIri));
348
349     queryString.append(sourceDelete.sparql(sparqlOptions));
350
351     // Compute the sync target of the resulting contact
352     const QString destinationSyncTarget = m_contactSyncTargets.value(targetUrn);
353     QSet<QString> syncTargets;
354
355     syncTargets.insert(destinationSyncTarget);
356
357     foreach(const QString &iri, sourceUrns) {
358         syncTargets.insert(m_contactSyncTargets.value(iri));
359     }
360
361     const QString preferredSyncTarget = pickPreferredSyncTarget(syncTargets, destinationSyncTarget);
362
363     // Abort early if pickPreferredSyncTarget() reported an error
364     if (lastError() != QContactManager::NoError) {
365         return QString();
366     }
367
368     static const QString syncTargetQueryTemplate = QString::fromLatin1(
369                 "DELETE {GRAPH <%3> {<%1> nie:generator ?generator}}"
370                 "WHERE {GRAPH <%3> {<%1> nie:generator ?generator}}"
371                 "INSERT {GRAPH <%3> {<%1> nie:generator %2}}"
372                 );
373     queryString.append(syncTargetQueryTemplate.arg(targetUrn,
374                                                   LiteralValue(preferredSyncTarget).sparql(),
375                                                   QtContactsTrackerDefaultGraphIri));
376
377     return queryString;
378 }
379
380 QString
381 QctContactMergeWorker::mergeContacts(const QString &targetUrn, const QString &sourceUrn)
382 {
383     const Options::SparqlOptions sparqlOptions = engine()->updateQueryOptions();
384
385     Insert insert;
386     ResourceValue target(targetUrn);
387     ResourceValue source(sourceUrn);
388
389     foreach (const QString &predicateUrn, m_contactPredicatesSingleWithGraph.values(sourceUrn)) {
390         if (m_contactPredicatesSingle.contains(targetUrn, predicateUrn) ||
391             m_contactPredicatesSingleWithGraph.contains(targetUrn, predicateUrn)) {
392             continue;
393         }
394
395         if (predicateUrn == nco::contactLocalUID::iri()) {
396             continue;
397         }
398
399         if (predicateUrn == nie::generator::iri()) {
400             continue;
401         }
402
403         Variable graphVar;
404         Graph dataGraph(graphVar);
405         Graph whereGraph(graphVar);
406         const ResourceValue predicate(predicateUrn);
407         Variable value;
408         dataGraph.addPattern(target, predicate, value);
409         insert.addData(dataGraph);
410         whereGraph.addPattern(source, predicate, value);
411         insert.addRestriction(whereGraph);
412
413         // Also fill the hash so that another "merge slave" will not try to write
414         // to this predicate
415         m_contactPredicatesSingle.insertMulti(targetUrn, predicateUrn);
416     }
417
418     foreach (const QString &predicateUrn, m_contactPredicatesSingle.values(sourceUrn)) {
419         if (m_contactPredicatesSingle.contains(targetUrn, predicateUrn) ||
420             m_contactPredicatesSingleWithGraph.contains(targetUrn, predicateUrn)) {
421             continue;
422         }
423
424         if (predicateUrn == nco::contactLocalUID::iri()) {
425             continue;
426         }
427
428         if (predicateUrn == nie::generator::iri()) {
429             continue;
430         }
431
432         const ResourceValue predicate(predicateUrn);
433         Variable value;
434         insert.addData(target, predicate, value);
435         insert.addRestriction(source, predicate, value);
436
437         // Also fill the hash so that another "merge slave" will not try to write
438         // to this predicate
439         m_contactPredicatesSingle.insertMulti(targetUrn, predicateUrn);
440     }
441
442
443     foreach (const QString &predicateUrn, m_contactPredicatesMultiWithGraph.values(sourceUrn).toSet()) {
444         if (predicateUrn == rdf::type::iri()) {
445             continue;
446         }
447
448         Variable graphVar;
449         Graph dataGraph(graphVar);
450         Graph whereGraph(graphVar);
451         ResourceValue predicate(predicateUrn);
452         Variable value;
453         dataGraph.addPattern(target, predicate, value);
454         insert.addData(dataGraph);
455         whereGraph.addPattern(source, predicate, value);
456         insert.addRestriction(whereGraph);
457     }
458
459     Insert ungraphedMultivaluesInsert;
460     foreach (const QString &predicateUrn, m_contactPredicatesMulti.values(sourceUrn).toSet()) {
461         if (predicateUrn == rdf::type::iri()) {
462             continue;
463         }
464
465         ResourceValue predicate(predicateUrn);
466         Variable value;
467         ungraphedMultivaluesInsert.addData(target, predicate, value);
468         ungraphedMultivaluesInsert.addRestriction(source, predicate, value);
469     }
470
471     return (insert.sparql(sparqlOptions) +
472             ungraphedMultivaluesInsert.sparql(sparqlOptions));
473 }
474
475 void
476 QctContactMergeWorker::run()
477 {
478     if (not turnIrreversible()
479             || not verifyRequest()
480             || not resolveContactIds()
481             || not fetchContactDetails()
482             || not doMerge()) {
483         return; // request failed (http://en.wikipedia.org/wiki/Lazy_evaluation)
484     }
485 }
486
487 void
488 QctContactMergeWorker::updateRequest(QContactManager::Error error)
489 {
490
491     engine()->updateContactRemoveRequest(staticCast(engine()->request(this).data()),
492                                          error, m_errorMap, QContactAbstractRequest::FinishedState);
493 }