Changes: Use QSparqlQueryOptions in runQuery
[qtcontacts-tracker:hasselmms-qtcontacts-tracker.git] / src / engine / contactunmergerequest.cpp
1 /** This file is part of QtContacts tracker storage plugin
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 "contactunmergerequest.h"
25
26 #include <dao/scalarquerybuilder.h>
27 #include <dao/sparqlresolver.h>
28 #include <dao/subject.h>
29 #include <dao/support.h>
30 #include <engine/engine.h>
31 #include <lib/customdetails.h>
32 #include <lib/unmergeimcontactsrequest.h>
33
34 #include <QtSparql>
35
36 CUBI_USE_NAMESPACE
37 CUBI_USE_NAMESPACE_RESOURCES
38
39 QTrackerContactSaveOrUnmergeRequest::QTrackerContactSaveOrUnmergeRequest(QContactAbstractRequest *request,
40                                                                          QContactTrackerEngine *engine,
41                                                                          QObject *parent)
42     : QTrackerBaseRequest<QctUnmergeIMContactsRequest>(engine, parent)
43     , m_unmergeOnlineAccounts(staticCast(request)->unmergeOnlineAccounts())
44     , m_sourceContact(staticCast(request)->sourceContact())
45 {
46 }
47
48 QTrackerContactSaveOrUnmergeRequest::~QTrackerContactSaveOrUnmergeRequest()
49 {
50 }
51
52 QString
53 QTrackerContactSaveOrUnmergeRequest::buildQuery()
54 {
55     QStringList queries;
56
57     foreach(const QContactOnlineAccount &account, m_unmergeOnlineAccounts) {
58         const QString contactIri(makeAnonymousIri(QUuid::createUuid()));
59
60         queries.append(insertContactQuery(contactIri, account).sparql());
61
62         // When unmerging contacts we add telepathy as a fallback generator, so
63         // that they get deleted properly when their hour comes
64         queries.append(insertTelepathyGeneratorFallbackQuery(contactIri).sparql());
65
66         foreach (const Delete &d, cleanupQueries(account)) {
67             queries.append(d.sparql());
68         }
69
70         m_unmergedContactIris += contactIri;
71     }
72
73     return queries.join(QLatin1String("\n"));
74 }
75
76 void
77 QTrackerContactSaveOrUnmergeRequest::run()
78 {
79     setCancelable(false);
80
81     if (canceled()) {
82         emitResultLater();
83         return;
84     }
85
86     if (resolveSourceContact() && fetchPredicates() &&
87         unmergeContacts() && resolveUnmergedContactIds()) {
88         emitResultLater();
89     }
90 }
91
92 bool
93 QTrackerContactSaveOrUnmergeRequest::resolveSourceContact()
94 {
95     if (m_sourceContact.id().managerUri() != engine()->managerUri() ||
96         m_sourceContact.localId() == 0) {
97         setLastError(QContactManager::BadArgumentError);
98         emitResultLater();
99         return false;
100     }
101
102     QctResourceIriResolver resolver(QList<QContactLocalId>() << m_sourceContact.localId());
103
104     if (not resolver.lookupAndWait()) {
105         reportError(resolver.errors(), QLatin1String("Cannot resolve resource IRI for source contact"));
106         return false;
107     }
108
109     if (resolver.resourceIris().count() != 1) {
110         setLastError(QContactManager::DoesNotExistError);
111         emitResultLater();
112         return false;
113     }
114
115     m_sourceContactIri = resolver.resourceIris().first();
116
117     return true;
118 }
119
120 bool
121 QTrackerContactSaveOrUnmergeRequest::resolveUnmergedContactIds()
122 {
123     QctTrackerIdResolver resolver(m_unmergedContactIris);
124
125     if (not resolver.lookupAndWait()) {
126         reportError(resolver.errors(), QLatin1String("Cannot resolve local ids of unmerged contacts"));
127         return false;
128     }
129
130     if (resolver.trackerIds().count() != m_unmergedContactIris.count()) {
131         reportError(QLatin1String("Failed to resolve local ids for all merged contacts"));
132         return false;
133     }
134
135     m_unmergedContactIds = resolver.trackerIds();
136
137     return true;
138 }
139
140 // can't make results a QVector<QVector<QString>> since QString::split returns
141 // QStringList
142 static QVector<QStringList>
143 parseResults(const QString &data)
144 {
145     const QStringList rows = data.split(QTrackerScalarContactQueryBuilder::detailSeparator());
146
147     QVector<QStringList> results;
148     results.reserve(rows.count());
149
150     foreach (const QString &row, rows) {
151         const QStringList tokens = row.split(QTrackerScalarContactQueryBuilder::fieldSeparator());
152
153         results.append(tokens);
154     }
155
156     return results;
157 }
158
159 bool
160 QTrackerContactSaveOrUnmergeRequest::fetchPredicates()
161 {
162     static const LiteralValue detailSeparator(QTrackerScalarContactQueryBuilder::detailSeparator());
163     static const LiteralValue fieldSeparator(QTrackerScalarContactQueryBuilder::fieldSeparator());
164
165     // We run two selects (nested in one)
166     // 1st one is to get the graph of all ?contact ?p ?o statements
167     // graph is either qct, or the telepathy iri, so we can use the graph to
168     // know which properties to detach from the original contact
169     //
170     // 2nd one is to enumerate the online accounts, because the graph for
171     // ?contact nco:hasAffiliation ?aff is either qct, telepathy iri or
172     // contactsd graph (if the affiliation has one IMAddress on it), so we need
173     // to check the actual IMAddress to know which affiliations we have to move
174
175     const ResourceValue contact(m_sourceContactIri);
176
177     // 1st select
178     const Variable predicate;
179     const Variable predicateGraph;
180
181     const ValueChain predicateProjections =
182             ValueChain() << predicateGraph << fieldSeparator
183                          << predicate;
184
185     Graph g(predicateGraph);
186     g.addPattern(contact, predicate, Variable());
187
188     Select predicateSelect;
189     predicateSelect.addProjection(Functions::groupConcat.
190                                   apply(Functions::concat.apply(predicateProjections),
191                                         detailSeparator));
192     predicateSelect.addRestriction(g);
193
194     // 2nd select
195
196     const Variable affiliationGraph;
197     const Variable affiliation;
198     const Variable imAddress;
199
200     const ValueChain accountProjections =
201             ValueChain() << affiliationGraph << fieldSeparator
202                          << affiliation << fieldSeparator
203                          << imAddress;
204
205     g = Graph(affiliationGraph);
206     g.addPattern(contact, nco::hasAffiliation::resource(), affiliation);
207     g.addPattern(affiliation, nco::hasIMAddress::resource(), imAddress);
208
209     Select accountSelect;
210     accountSelect.addProjection(Functions::groupConcat.
211                                 apply(Functions::concat.apply(accountProjections),
212                                       detailSeparator));
213     accountSelect.addRestriction(g);
214
215     // Final step, combine the two selects in one
216     Select select;
217     select.addProjection(predicateSelect);
218     select.addProjection(accountSelect);
219
220     const QSparqlQuery query(select.sparql(), QSparqlQuery::SelectStatement);
221     QScopedPointer<QSparqlResult> result(runQuery(query, SyncQueryOptions));
222
223     if (result.isNull()) {
224         return false; // runQuery() calls reportError()
225     }
226
227     while (result->next()) {
228         const QString predicateData = result->stringValue(0);
229         const QString accountData = result->stringValue(1);
230
231         // We first get the list of predicate/graph
232         foreach (const QStringList &row, parseResults(predicateData)) {
233             if (row.size() != 2) {
234                 qctWarn("Skipping invalid result row");
235                 continue;
236             }
237
238             // Skip predicates that might blow up the source contact
239             if (row.at(1) == rdf::type::iri()) {
240                 continue;
241             }
242
243             m_predicates.insert(row.at(0), row.at(1));
244         }
245
246         // And then the list of affiliation graph/affiliation iri/imaddress iri
247         foreach (const QStringList &row, parseResults(accountData)) {
248             if (row.size() != 3) {
249                 qctWarn("Skipping invalid result row");
250                 continue;
251             }
252
253             m_onlineAccounts.insert(row.at(2), qMakePair(row.at(0), row.at(1)));
254         }
255     }
256
257     return true;
258 }
259
260 bool
261 QTrackerContactSaveOrUnmergeRequest::unmergeContacts()
262 {
263     const QSparqlQuery unmergeQuery(buildQuery(), QSparqlQuery::InsertStatement);
264     return not QScopedPointer<QSparqlResult>(runQuery(unmergeQuery, SyncQueryOptions)).isNull();
265 }
266
267 Insert
268 QTrackerContactSaveOrUnmergeRequest::insertContactQuery(const QString &contactIri,
269                                                         const QContactOnlineAccount &account)
270 {
271     const ResourceValue contact(contactIri);
272     const QString accountPath = account.value(QContactOnlineAccount__FieldAccountPath);
273     const QString accountUri = account.accountUri();
274     const QString telepathyIri = makeTelepathyIri(accountPath, accountUri);
275
276     Insert insert;
277
278     // We first reparent all the hasAffiliation statements that were in the telepathy graph (if any)
279     const QSet<QString> telepathyPredicates = m_predicates.values(telepathyIri).toSet();
280
281     Graph commonGraph = Graph(ResourceValue(engine()->graphIri()));
282
283     commonGraph.addPattern(contact, rdf::type::resource(), nco::PersonContact::resource());
284     commonGraph.addPattern(contact, nie::contentLastModified::resource(), LiteralValue(QDateTime::currentDateTimeUtc()));
285     insert.addData(commonGraph);
286
287     if (not telepathyPredicates.isEmpty()) {
288         Graph imGraph = Graph(ResourceValue(telepathyIri));
289         Graph restrictionGraph = Graph(ResourceValue(telepathyIri));
290         const ResourceValue source(m_sourceContactIri);
291
292         foreach (const QString &predicate, telepathyPredicates) {
293             const ResourceValue predicateResource(predicate);
294             Variable value;
295
296             imGraph.addPattern(contact, predicateResource, value);
297             restrictionGraph.addPattern(source, predicateResource, value);
298         }
299
300         insert.addData(imGraph);
301         insert.addRestriction(restrictionGraph);
302     }
303
304     // Then reparent the affiliation (if any) that was linking to the IMAddress
305     if (m_onlineAccounts.contains(telepathyIri)) {
306         const QPair<QString, QString> accountData = m_onlineAccounts.value(telepathyIri);
307         Graph imAffiliationGraph = Graph(ResourceValue(accountData.first));
308         const ResourceValue affiliation(accountData.second);
309
310         imAffiliationGraph.addPattern(contact, nco::hasAffiliation::resource(), affiliation);
311
312         insert.addData(imAffiliationGraph);
313     }
314
315     return insert;
316 }
317
318 QList<Delete>
319 QTrackerContactSaveOrUnmergeRequest::cleanupQueries(const QContactOnlineAccount &account)
320 {
321     const ResourceValue source(m_sourceContactIri);
322     const QString accountPath = account.value(QContactOnlineAccount__FieldAccountPath);
323     const QString accountUri = account.accountUri();
324     const QString telepathyIri = makeTelepathyIri(accountPath, accountUri);
325
326     QList<Cubi::Delete> results;
327
328     Delete d;
329
330     // Delete all properties when statement is in the graph of unmerged IMAddress
331     if (m_predicates.contains(telepathyIri)) {
332         Graph deleteGraph = Graph(ResourceValue(telepathyIri));
333         Graph whereGraph = Graph(ResourceValue(telepathyIri));
334
335         foreach (const QString &predicate, m_predicates.values(telepathyIri).toSet()) {
336             ResourceValue predicateResource = ResourceValue(predicate);
337
338             // Avoid predicates that might blow everything up
339             if (predicateResource == rdf::type::resource()) {
340                 continue;
341             }
342
343             Variable value;
344
345             deleteGraph.addPattern(source, predicateResource, value);
346             whereGraph.addPattern(source, predicateResource, value);
347         }
348
349         d.addData(deleteGraph);
350         d.addRestriction(whereGraph);
351     }
352
353     results.append(d);
354
355     // We do the delete in two parts, because the restrictions are different (if
356     // the where for the first one does not match, we still want to try to delete
357     // a potential IMAddress
358
359     d = Delete();
360
361     // And also the IMAddress itself
362     if (m_onlineAccounts.contains(telepathyIri)) {
363         const QPair<QString, QString> accountData = m_onlineAccounts.value(telepathyIri);
364
365         d.addData(source, nco::hasAffiliation::resource(), ResourceValue(accountData.second));
366     }
367
368     results.append(d);
369
370     return results;
371 }
372
373 Insert
374 QTrackerContactSaveOrUnmergeRequest::insertTelepathyGeneratorFallbackQuery(const QString &contactIri)
375 {
376     static const LiteralValue telepathyGenerator = LiteralValue(QString::fromLatin1("telepathy"));
377
378     Insert insert;
379     const ResourceValue subject(contactIri);
380     PatternGroup restriction;
381     PatternGroup optionalGenerator;
382     Variable generator;
383
384     insert.addData(subject, nie::generator::resource(), telepathyGenerator);
385
386     restriction.addPattern(subject, rdf::type::resource(), nco::PersonContact::resource());
387     optionalGenerator.setOptional(true);
388     optionalGenerator.addPattern(subject, nie::generator::resource(), generator);
389     restriction.addPattern(optionalGenerator);
390     restriction.setFilter(Functions::not_.apply(Functions::bound.apply(generator)));
391     insert.addRestriction(restriction);
392
393     return insert;
394 }
395
396 void
397 QTrackerContactSaveOrUnmergeRequest::emitResult(QContactManager::Error error)
398 {
399     const QctRequestLocker &request = engine()->request(this);
400
401     if (not request.isNull()) {
402         staticCast(request.data())->setUnmergedContactIds(m_unmergedContactIds);
403
404         engine()->updateContactSaveRequest(staticCast(request.data()),
405                                            QList<QContact>(), error, ErrorMap(),
406                                            QContactAbstractRequest::FinishedState);
407     }
408 }
409
410
411 ///////////////////////////////////////////////////////////////////////////////////////////////////
412
413 #include "moc_contactunmergerequest.cpp"