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