Changes: Create an nco:Affiliation for each context but Other context.
[qtcontacts-tracker:hasselmms-qtcontacts-tracker.git] / src / engine / contactsaverequest.cpp
1 /****************************************************************************
2 **
3 ** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
4 ** All rights reserved.
5 ** Contact: Nokia Corporation (qt-info@nokia.com)
6 **
7 ** This file is part of the Qt Mobility Components.
8 **
9 ** $QT_BEGIN_LICENSE:LGPL$
10 ** No Commercial Usage
11 ** This file contains pre-release code and may not be distributed.
12 ** You may use this file in accordance with the terms and conditions
13 ** contained in the Technology Preview License Agreement accompanying
14 ** this package.
15 **
16 ** GNU Lesser General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU Lesser
18 ** General Public License version 2.1 as published by the Free Software
19 ** Foundation and appearing in the file LICENSE.LGPL included in the
20 ** packaging of this file.  Please review the following information to
21 ** ensure the GNU Lesser General Public License version 2.1 requirements
22 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
23 **
24 ** In addition, as a special exception, Nokia gives you certain additional
25 ** rights.  These rights are described in the Nokia Qt LGPL Exception
26 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
27 **
28 ** If you have questions regarding the use of this file, please contact
29 ** Nokia at qt-info@nokia.com.
30 **
31 **
32 **
33 **
34 **
35 **
36 **
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41
42 #include "contactsaverequest.h"
43 #include "engine.h"
44
45 #include <dao/classhierarchy.h>
46 #include <dao/contactdetail.h>
47 #include <dao/contactdetailschema.h>
48 #include <dao/customdetails.h>
49 #include <dao/querybuilder.h>
50 #include <dao/settings.h>
51 #include <dao/subject.h>
52 #include <dao/support.h>
53
54 #include <dbus/tracker.h>
55
56 #include <QtGui/QImageWriter>
57
58 #include <QtTracker/ontologies/nao.h>
59 #include <QtTracker/ontologies/nie.h>
60
61 #include <errno.h>
62
63 ///////////////////////////////////////////////////////////////////////////////////////////////////
64
65 using namespace SopranoLive;
66 using namespace SopranoLive::Ontologies;
67
68 ///////////////////////////////////////////////////////////////////////////////////////////////////
69
70 typedef QMap<QString, RDFVariable> AffiliationMap;
71 typedef QList<class DetailMapping> DetailMappingList;
72 typedef QMultiHash<PredicateChain, const QUrl *> EntityHash;
73
74 ///////////////////////////////////////////////////////////////////////////////////////////////////
75
76 class DetailMappingData : public QSharedData
77 {
78     friend class DetailMapping;
79
80 public: // constructors
81     DetailMappingData(const QContactDetail &genericDetail,
82                       const QTrackerContactDetail *trackerDetail)
83         : m_genericDetail(genericDetail)
84         , m_trackerDetail(trackerDetail)
85         , m_contextsNormalized(false)
86     {
87     }
88
89 private: // fields
90     QContactDetail                  m_genericDetail;
91     const QTrackerContactDetail *   m_trackerDetail;
92     bool                            m_contextsNormalized : 1;
93 };
94
95 ///////////////////////////////////////////////////////////////////////////////////////////////////
96
97 class DetailMapping
98 {
99 public: // constructors
100     DetailMapping(const QContactDetail &genericDetail,
101                   const QTrackerContactDetail *trackerDetail)
102         : d(new DetailMappingData(genericDetail, trackerDetail))
103     {
104     }
105
106 public: // attributes
107     /// a detail which has no own ontology representation
108     bool isCustomDetail() const;
109
110     /// is a detail which is generated from other details and not stored itself
111     bool isSyntheticDetail() const;
112
113     QString definitionName() const;
114     QStringList contexts() const;
115
116     QString detailUri() const;
117     QStringList linkedDetailUris() const;
118
119     QVariant fieldValue(const QString &fieldName) const;
120     QVariantMap fieldValues() const;
121
122     const QList<QTrackerContactDetailField> & trackerFields() const;
123     const QTrackerContactDetailField * subTypeField() const;
124     QTrackerContactSubject::Scheme detailScheme() const;
125
126 public: // methods
127     bool updateDetailUri(QContactLocalId contactId, const DetailMappingList &mappings);
128
129 private: // fields
130     QExplicitlySharedDataPointer<DetailMappingData> d;
131 };
132
133 ///////////////////////////////////////////////////////////////////////////////////////////////////
134
135 class UpdateBuilder
136 {
137 public:
138     UpdateBuilder(const QTrackerContactSaveRequest *request,
139                   const QContact &contact, const QDateTime &timestamp);
140
141     const QString & guid() const { return m_guid; }
142     bool hadGuid() const { return m_hadGuid; }
143
144     const QDateTime & accessed() const { return m_accessed; }
145     bool hadAccessed() const { return m_hadAccessed; }
146
147     const QDateTime & created() const { return m_created; }
148     bool hadCreated() const { return m_hadCreated; }
149
150     const QDateTime & lastModified() const { return m_lastModified; }
151     bool hadLastModified() const { return m_hadLastModified; }
152
153     const DetailMappingList & mappings() const { return m_mappings; }
154     const QContactLocalId & contactLocalId() const { return m_contactLocalId; }
155     const QTrackerContactDetailSchema & schema() const { return m_schema; }
156     const QTrackerClassHierarchy & classes() const { return m_classes; }
157
158     bool isSelfContact() const { return m_isSelfContact; }
159
160 public:
161     void collectInsertions(RDFUpdate &update);
162     void collectDeletions(RDFUpdate &update) const;
163
164 protected:
165     RDFVariable & lookupAffiliation(const QString &context);
166
167     void restrictAffiliation(const RDFVariable &contact,
168                              const RDFVariable &affiliation,
169                              const QString &context);
170
171     QUrl makeIri(QTrackerContactSubject::Scheme scheme, const QVariantList &values) const;
172     QString makeUniqueName(const QString &basename) const;
173
174     void insert(const RDFVariable &subject, const RDFVariable &predicate,
175                 const RDFVariable &value, bool explicitValue = true);
176
177     void insertCustomValues(const RDFVariable &subject, const QString &detailName,
178                             const QString &fieldName, const QVariant &value,
179                             const QVariantList &allowableValues = QVariantList());
180     void insertCustomValues(const RDFVariable &subject, const QString &detailName,
181                             const QTrackerContactDetailField &field, const QVariant &value);
182
183     void insertCustomDetail(RDFVariable &subject, const DetailMapping &detail);
184
185     void insertDetailField(QContactLocalId contactId,
186                            const RDFVariable &subject, const DetailMapping &detail,
187                            const QTrackerContactDetailField &field, const QVariant &value,
188                            const EntityHash &entities, PredicateVariableHash &objectCache);
189
190     void deleteContactProperties(RDFUpdate &update) const;
191     void deleteAssociatedObjects(RDFUpdate &update) const;
192     void deleteCustomDetails(RDFUpdate &update) const;
193
194     const QctLogger & qctLogger() const { return m_request->qctLogger(); }
195
196 private:
197     const QTrackerContactSaveRequest *const m_request;
198
199     QString m_guid;
200     QDateTime m_accessed;
201     QDateTime m_created;
202     QDateTime m_lastModified;
203     DetailMappingList m_mappings;
204     QContactLocalId m_contactLocalId;
205     const QTrackerContactDetailSchema &m_schema;
206     const QTrackerClassHierarchy &m_classes;
207
208     RDFStatementList m_explicitInserts;
209     RDFStatementList m_implicitInserts;
210
211     AffiliationMap m_affiliations;
212
213     bool m_hadGuid : 1;
214     bool m_hadAccessed : 1;
215     bool m_hadCreated : 1;
216     bool m_hadLastModified : 1;
217     bool m_isSelfContact : 1;
218 };
219
220 ///////////////////////////////////////////////////////////////////////////////////////////////////
221
222 class QTrackerContactSaveRequestTask : public QObject
223 {
224     Q_DISABLE_COPY(QTrackerContactSaveRequestTask);
225     Q_OBJECT;
226
227 public:
228     QTrackerContactSaveRequestTask(const QDBusPendingCall &call, int offset, QObject *parent = 0);
229
230 signals:
231     void success(int offset);
232     void failure(int offset, QString const &message);
233
234 private slots:
235     void finished();
236
237 private:
238     QDBusPendingCallWatcher m_call;
239     const int m_offset;
240 };
241
242 ///////////////////////////////////////////////////////////////////////////////////////////////////
243
244 template <typename T>
245 inline bool operator<(const QList<T> &a, const QList<T> &b)
246 {
247     for(int i = 0; i < a.count() && i < b.count(); ++i) {
248         if (a[i] < b[i]) {
249             return true;
250         } else if (b[i] < a[i]) {
251             return false;
252         }
253     }
254
255     return a.count() < b.count();
256 }
257
258 ///////////////////////////////////////////////////////////////////////////////////////////////////
259
260 inline bool
261 DetailMapping::isCustomDetail() const
262 {
263     return (0 == d->m_trackerDetail);
264 }
265
266 inline bool
267 DetailMapping::isSyntheticDetail() const
268 {
269     return (0 != d->m_trackerDetail && d->m_trackerDetail->isSynthetic());
270 }
271
272 inline QStringList
273 DetailMapping::contexts() const
274 {
275     if (0 != d->m_trackerDetail && not d->m_trackerDetail->hasContext()) {
276         return QStringList();
277     }
278
279     if (not d->m_contextsNormalized) {
280         QStringList normalizedContexts;
281
282         foreach(const QString &context, d->m_genericDetail.contexts().toSet()) {
283             normalizedContexts += qctCamelCase(context);
284         }
285
286         d->m_genericDetail.setContexts(normalizedContexts);
287         d->m_contextsNormalized = true;
288     }
289
290     return d->m_genericDetail.contexts();
291 }
292
293 inline QString
294 DetailMapping::definitionName() const
295 {
296     return d->m_genericDetail.definitionName();
297 }
298
299 inline QString
300 DetailMapping::detailUri() const
301 {
302     return d->m_genericDetail.detailUri();
303 }
304
305 inline QStringList
306 DetailMapping::linkedDetailUris() const
307 {
308     return d->m_genericDetail.linkedDetailUris();
309 }
310
311 inline QVariant
312 DetailMapping::fieldValue(const QString &fieldName) const
313 {
314     return d->m_genericDetail.variantValue(fieldName);
315 }
316
317 inline QVariantMap
318 DetailMapping::fieldValues() const
319 {
320     return d->m_genericDetail.variantValues();
321 }
322
323 inline const QList<QTrackerContactDetailField> &
324 DetailMapping::trackerFields() const
325 {
326     return d->m_trackerDetail->fields();
327 }
328
329 inline const QTrackerContactDetailField *
330 DetailMapping::subTypeField() const
331 {
332     return d->m_trackerDetail->subTypeField();
333 }
334
335 inline QTrackerContactSubject::Scheme
336 DetailMapping::detailScheme() const
337 {
338     return d->m_trackerDetail->detailScheme();
339 }
340
341 bool
342 DetailMapping::updateDetailUri(QContactLocalId contactId, const DetailMappingList &mappings)
343 {
344     // skip custom details
345     if (isCustomDetail()) {
346         return false;
347     }
348
349     QTrackerContactSubject::Scheme scheme = QTrackerContactSubject::Anonymous;
350     const QString oldDetailUri = detailUri();
351     QVariantList values;
352
353     // build new detail URI from current detail content
354     foreach(const QTrackerContactDetailField &f, trackerFields()) {
355         if (f.hasDetailUri()) {
356             if (values.isEmpty()) {
357                 scheme = f.propertyChain().last().subjectScheme();
358             }
359
360             values.append(fieldValue(f.name()));
361         }
362     }
363
364     // detail URI doesn't use field values, abort
365     if (values.isEmpty()) {
366         return false;
367     }
368
369     // construct new detail URI
370     const QUrl subjectIri = QTrackerContactSubject::makeIri(scheme, contactId, values);
371     const QString newDetailUri = subjectIri.toString();
372
373     // abort if nothing changed
374     if (newDetailUri == oldDetailUri) {
375         return false;
376     }
377
378     // update detail URI
379     d->m_genericDetail.setDetailUri(newDetailUri);
380
381     // update details pointing to this one
382     foreach(const DetailMapping &otherDetail, mappings) {
383         QStringList linkedDetails = otherDetail.linkedDetailUris();
384
385         // try to remove old URI from list of linked details
386         if (linkedDetails.removeOne(oldDetailUri)) {
387             // detail was linked, therefore update the link
388             otherDetail.d->m_genericDetail.setLinkedDetailUris(linkedDetails << newDetailUri);
389         }
390     }
391
392     return true;
393 }
394
395 ////////////////////////////////////////////////////////////////////////////////////////////////////
396
397 QTrackerContactSaveRequestTask::QTrackerContactSaveRequestTask(const QDBusPendingCall &call,
398                                                                int offset, QObject *parent)
399     : QObject(parent)
400     , m_call(call)
401     , m_offset(offset)
402 {
403     connect(&m_call, SIGNAL(finished(QDBusPendingCallWatcher*)), SLOT(finished()));
404 }
405
406 void
407 QTrackerContactSaveRequestTask::finished()
408 {
409     if (m_call.isError()) {
410         emit failure(m_offset, m_call.error().message());
411     } else {
412         emit success(m_offset);
413     }
414 }
415
416 ///////////////////////////////////////////////////////////////////////////////////////////////////
417
418 QTrackerContactSaveRequest::QTrackerContactSaveRequest(QContactAbstractRequest *request,
419                                                        QContactTrackerEngine *engine,
420                                                        QObject *parent) :
421     QTrackerBaseRequest<QContactSaveRequest>(request, engine, parent),
422     m_timestamp(QDateTime::currentDateTime()),
423     m_effectiveError(QContactManager::UnspecifiedError),
424     m_contactOffset(0), m_batchSize(0)
425 {
426     m_contacts.append(m_request->contacts());
427 }
428
429 QTrackerContactSaveRequest::~QTrackerContactSaveRequest()
430 {
431 }
432
433 ///////////////////////////////////////////////////////////////////////////////////////////////////
434
435 static void
436 addEntity(const QTrackerClassHierarchy &classes, EntityHash &entities,
437           const PredicateChain &predicates, const QUrl &newIri)
438 {
439     foreach(const QUrl *const oldIri, entities.values(predicates)) {
440         if (classes.isSubClassOf(*oldIri, newIri)) {
441             // skipping already covered entity type
442             return;
443         }
444
445         if (classes.isSubClassOf(newIri, *oldIri)) {
446             // removing too generic entity type which is covered by newIri
447             entities.remove(predicates, oldIri);
448         }
449     }
450
451     // adding the new entity type
452     entities.insertMulti(predicates, &newIri);
453 }
454
455 static EntityHash
456 collectEntities(const QTrackerClassHierarchy &classes, const DetailMapping &detail)
457 {
458     EntityHash entities;
459
460     foreach(const QTrackerContactDetailField &field, detail.trackerFields()) {
461         if (field.propertyChain().isEmpty()) {
462             continue;
463         }
464
465         PredicateChain predicates;
466
467         const PropertyInfoList::ConstIterator end(field.propertyChain().end() - 1);
468         for(PropertyInfoList::ConstIterator i(field.propertyChain().begin()); i != end; ++i) {
469             const QUrl &predicateIri(i->iri());
470             const PropertyInfoBase &pi(*(i + 1));
471
472             if (pi.inverse()) {
473                 addEntity(classes, entities, predicates << predicateIri, pi.rangeIri());
474             } else {
475                 addEntity(classes, entities, predicates << predicateIri, pi.domainIri());
476             }
477         }
478
479         if (field.hasSubTypeClasses()) {
480             QVariant subTypeValue(detail.fieldValue(field.name()));
481             QSet<QString> subTypes;
482
483             if (subTypeValue.isNull() && not field.defaultValue().isNull()) {
484                 subTypeValue = field.defaultValue();
485             }
486
487             switch(subTypeValue.type()) {
488             case QVariant::String:
489                 subTypes.insert(subTypeValue.toString());
490                 break;
491
492             case QVariant::StringList:
493                 subTypes.unite(subTypeValue.toStringList().toSet());
494                 break;
495
496             default:
497                 qctWarn(QString::fromLatin1("Invalid type %1 for subtype field %3 of %2 detail").
498                         arg(subTypeValue.typeName(), detail.definitionName(), field.name()));
499                 continue;
500             }
501
502             predicates.append(field.propertyChain().last().iri());
503
504             foreach(const ClassInfoBase &i, field.subTypeClasses()) {
505                 if (subTypes.contains(i.text())) {
506                     addEntity(classes, entities, predicates, i.iri());
507                 }
508             }
509         }
510     }
511
512     return entities;
513 }
514
515 static QVariant
516 findSubTypeValue(const DetailMapping &detail, const QTrackerContactDetailField *const subTypeField)
517 {
518     QVariant value;
519
520     if (0 != subTypeField && subTypeField->hasSubTypeProperties()) {
521         value = detail.fieldValue(subTypeField->name());
522
523         if (QVariant::String != value.type()) {
524             qctWarn(QString::fromLatin1("Invalid type %1 for subtype field %3 of %2 detail").
525                     arg(value.typeName(), detail.definitionName(), subTypeField->name()));
526             value = QVariant();
527         }
528     }
529
530     return value;
531
532 }
533
534 static const QUrl &
535 findSubTypePredicate(const DetailMapping &detail, const QTrackerContactDetailField &field,
536                      const QTrackerContactDetailField *const subTypeField)
537 {
538     const QVariant subTypeValue = findSubTypeValue(detail, subTypeField);
539
540     if (subTypeValue.isValid()) {
541         foreach(const PropertyInfoBase &pi, subTypeField->subTypeProperties()) {
542             if (pi.value() == subTypeValue) {
543                 return pi.iri();
544             }
545         }
546     }
547
548     return field.propertyChain().last().iri();
549 }
550
551 QString
552 UpdateBuilder::makeUniqueName(const QString &basename) const
553 {
554     return basename + QString::number(m_explicitInserts.count());
555 }
556
557 void
558 UpdateBuilder::insertCustomValues(const RDFVariable &subject,
559                                   const QString &detailName, const QString &fieldName,
560                                   const QVariant &value, const QVariantList &allowableValues)
561 {
562     QVariantList customValues;
563
564     switch(value.type()) {
565     case QVariant::StringList:
566         foreach(const QString &element, value.toStringList()) {
567             if (not element.isEmpty() && not allowableValues.contains(element)) {
568                 customValues.append(element);
569             }
570         }
571
572         break;
573
574     case QVariant::List:
575         foreach(const QVariant &element, value.toList()) {
576             if (not element.isNull() && not allowableValues.contains(element)) {
577                 customValues.append(element);
578             }
579         }
580
581         break;
582
583     default:
584         if (not value.isNull() && not allowableValues.contains(value)) {
585             customValues.append(value);
586         }
587     }
588
589     foreach(const QVariant &element, customValues) {
590         RDFVariable property(makeUniqueName(QTrackerContactQueryBuilder::name(detailName, fieldName)));
591
592         insert(subject, rdf::type::iri(), nie::InformationElement::iri());
593         insert(subject, nao::hasProperty::iri(), property);
594
595         insert(property, rdf::type::iri(), nao::Property::iri());
596         insert(property, nao::propertyName::iri(), LiteralValue(fieldName));
597         insert(property, nao::propertyValue::iri(), QTrackerContactQueryBuilder::value(element));
598     }
599 }
600
601 void
602 UpdateBuilder::insertCustomValues(const RDFVariable &subject, const QString &detailName,
603                                   const QTrackerContactDetailField &field,
604                                   const QVariant &value)
605 {
606     insertCustomValues(subject, detailName, field.name(), value,
607                        field.describeAllowableValues());
608 }
609
610 static PropertyInfoList::ConstIterator
611 endOfPropertyChain(const QTrackerContactDetailField &field)
612 {
613     if (field.isWithoutMapping()) {
614         return field.propertyChain().end();
615     }
616
617     return field.propertyChain().end() - 1;
618 }
619
620 void
621 UpdateBuilder::insertDetailField(QContactLocalId contactId,
622                                  const RDFVariable &subject, const DetailMapping &detail,
623                                  const QTrackerContactDetailField &field, const QVariant &value,
624                                  const EntityHash &entities, PredicateVariableHash &objectCache)
625 {
626     if (not value.isValid()) {
627         qctWarn(QString::fromLatin1("Value must be valid for %2 field of %1 detail").
628                 arg(detail.definitionName(), field.name()));
629         return;
630     }
631
632     const QTrackerContactDetailField *const subTypeField = detail.subTypeField();
633     const PropertyInfoList &propertyChain = field.propertyChain();
634
635     if (propertyChain.isEmpty()) {
636         qctWarn(QString::fromLatin1("RDF property chain needed for %2 field of %1 detail").
637                 arg(detail.definitionName(), field.name()));
638         return;
639     }
640
641     if (propertyChain.first().readOnly()) {
642         return;
643     }
644
645     PredicateChain predicateChain;
646     RDFVariableList axis;
647     axis.append(subject);
648
649     const PropertyInfoList::ConstIterator end = endOfPropertyChain(field);
650     const PropertyInfoList::ConstIterator tail = propertyChain.end() - 1;
651
652     for(PropertyInfoList::ConstIterator pi = propertyChain.begin(); pi != end; ++pi) {
653         if (pi->readOnly()) {
654             break;
655         }
656
657         PredicateVariableHash::ConstIterator object = objectCache.find(predicateChain << pi->iri());
658
659         if (object == objectCache.end()) {
660             // figure out subject IRI for this field
661             QUrl subjectIri;
662
663             const PropertyInfoList::ConstIterator npi = pi + 1;
664
665             if (npi != propertyChain.end() && npi->hasSubjectScheme()) {
666                 // Prefer the detail's explicit URI for tail nodes, but only if the
667                 // property's subject scheme matches the detail's scheme. This additional
668                 // check is needed because details like Organization randomly spread their
669                 // fields over many different entities.
670                 if (npi == tail && npi->subjectScheme() == detail.detailScheme()) {
671                     subjectIri = detail.detailUri();
672                 }
673
674                 // If we don't have an entity IRI yet generate it from property and value.
675                 if (subjectIri.isEmpty()) {
676                     subjectIri = npi->makeSubject(contactId, value);
677                 }
678             }
679
680             // name object variable and assign subject IRI
681             RDFVariable objectVariable;
682
683             if (subjectIri.isEmpty()) {
684                 // create named blank variable if no subject IRI could be build
685                 const QString name = makeUniqueName(detail.definitionName());
686                 objectVariable.metaAssign(RDFVariable(name));
687             } else {
688                 // assign generated subject IRI
689                 objectVariable.metaAssign(subjectIri);
690             }
691
692             // assign RDF classes to this field
693             EntityHash::ConstIterator entity = entities.find(predicateChain);
694
695             if (entities.end() == entity) {
696                 qctWarn(QString::fromLatin1("Cannot find entities for %3 property of %1 detail's %2 field").
697                         arg(detail.definitionName(), field.name(), predicateChain.last().toString()));
698             }
699
700             for(; entity != entities.end() && entity.key() == predicateChain; ++entity) {
701                 // create insert statement of this entity type
702                 object = objectCache.insert(predicateChain, objectVariable);
703                 insert(*object, rdf::type::iri(), **entity);
704             }
705         }
706
707         // create insert statement with the field's value
708         insert(axis.last(), pi->iri(), *object);
709         axis.append(m_explicitInserts.last().object());
710
711         // insert custom values for subTypes field
712         if (subTypeField && subTypeField->permitsCustomValues()) {
713             insertCustomValues(axis.last(), detail.definitionName(), *subTypeField,
714                                detail.fieldValue(subTypeField->name()));
715         }
716     }
717
718     // find proper type of the value to insert
719     if (not field.isWithoutMapping()) {
720         const QUrl subTypePredicate = findSubTypePredicate(detail, field, subTypeField);
721         insert(axis.last(), subTypePredicate, QTrackerContactQueryBuilder::value(value));
722     }
723
724     // insert computed values
725     foreach(const PropertyInfoBase &pi, field.computedProperties()) {
726         QVariant computedValue;
727
728         if (pi.conversion()->makeValue(value, computedValue)) {
729             insert(axis.last(), pi.iri(), QTrackerContactQueryBuilder::value(computedValue));
730         }
731     }
732
733     // store custom values
734     if (field.permitsCustomValues()) {
735         insertCustomValues(axis.last(), detail.definitionName(), field, value);
736     }
737 }
738
739 UpdateBuilder::UpdateBuilder(const QTrackerContactSaveRequest *request,
740                              const QContact &contact, const QDateTime &timestamp)
741     : m_request(request)
742     , m_schema(request->engine()->schema(contact.type()))
743     , m_classes(request->engine()->classes())
744 {
745     // figure out the contact's timestamps
746    QContactTimestamp timestampDetail(contact.detail<QContactTimestamp>());
747
748    m_accessed = timestampDetail.variantValue(QContactTimestamp__FieldAccessedTimestamp).toDateTime();
749    m_hadAccessed = not m_accessed.isNull();
750
751    if (m_accessed.isNull()) {
752        m_accessed = timestamp;
753    }
754
755    m_created = timestampDetail.created();
756    m_hadCreated = not m_created.isNull();
757
758    if (m_created.isNull()) {
759        m_created = timestamp;
760    }
761
762    m_lastModified = timestampDetail.lastModified();
763    m_hadLastModified = not m_lastModified.isNull();
764
765    if (m_lastModified.isNull() || 0 != contact.localId()) {
766        // only preserve the lastModified field for new contacts...
767        m_lastModified = timestamp;
768    }
769
770    // figure out the contact's GUID
771    QContactGuid guidDetail(contact.detail<QContactGuid>());
772
773    m_guid = guidDetail.guid();
774    m_hadGuid = not m_guid.isEmpty();
775
776    if (m_guid.isEmpty()) {
777        m_guid = qctUuidString();
778    }
779
780    // calculate local contact ID
781    m_contactLocalId = contact.localId();
782
783    if (0 == m_contactLocalId) {
784        m_contactLocalId = qHash(guid());
785    }
786
787    m_isSelfContact = (m_contactLocalId == request->engine()->selfContactId(0));
788
789    // collect details and update their URIs
790    foreach(const QContactDetail &detail, contact.details()) {
791         const QTrackerContactDetail *definition(m_schema.detail(detail.definitionName()));
792
793         if (0 == definition) {
794             if (m_schema.isSyntheticDetail(detail.definitionName())) {
795                 continue; // don't even store synthesized details as nao:Property
796             }
797         }
798
799         m_mappings.append(DetailMapping(detail, definition));
800     }
801
802    foreach(DetailMapping detail, m_mappings) {
803        detail.updateDetailUri(contactLocalId(), m_mappings);
804    }
805 }
806
807 void
808 UpdateBuilder::insert(const RDFVariable &subject, const RDFVariable &predicate,
809                       const RDFVariable &value, bool explicitValue)
810 {
811     if (explicitValue) {
812         m_explicitInserts += RDFStatement(subject, predicate, value);
813     } else {
814         RDFVariable restricted = subject.metaValue().iri();
815         RDFVariable property = restricted.optional().property(predicate);
816
817         m_implicitInserts += RDFStatement(restricted, predicate, value);
818
819         not restricted.variable(property).isBound();
820     }
821 }
822
823 void
824 UpdateBuilder::insertCustomDetail(RDFVariable &subject, const DetailMapping &detail)
825 {
826     const QString detailName = detail.definitionName();
827     const QVariantMap fields = detail.fieldValues();
828
829     RDFVariable detailProperty(makeUniqueName(detailName));
830
831     insert(subject, nao::hasProperty::iri(), detailProperty);
832     insert(detailProperty, rdf::type::iri(), nao::Property::iri());
833     insert(detailProperty, nao::propertyName::iri(), LiteralValue(detailName));
834
835     for(QVariantMap::ConstIterator i = fields.begin(); i != fields.end(); ++i) {
836         insertCustomValues(detailProperty, detailName, i.key(), i.value());
837     }
838 }
839
840 void
841 UpdateBuilder::restrictAffiliation(const RDFVariable &contact,
842                                    const RDFVariable &affiliation,
843                                    const QString &context)
844 {
845     insert(affiliation, rdf::type::iri(), nco::Affiliation::iri());
846     insert(affiliation, rdfs::label::iri(), LiteralValue(context));
847     insert(contact, nco::hasAffiliation::iri(), affiliation);
848 }
849
850 void
851 UpdateBuilder::collectInsertions(RDFUpdate &update)
852 {
853     // Removing Role properties also removed the contact's PersonContact type,
854     // so we have to restore it.
855     RDFVariable contact = schema().makeContactIri(contactLocalId());
856
857     // Restore the resource type, we removed it for quick propertly deletion.
858     foreach(const QUrl &classIri, schema().contactClassIris()) {
859         insert(contact, rdf::type::iri(), classIri);
860     }
861
862     // Always update the local UID to support a-priori contacts like the self contact.
863     // Accept update errors if that property already exists with different value as
864     // this property is assumed to be immutable.
865     const LiteralValue contactLocalUID(QString::number(contactLocalId()));
866     insert(contact, nco::contactLocalUID::iri(), contactLocalUID);
867
868     // Prepare affiliation for work context in case the contact has an organization detail.
869     // This one should be stored with the same affiliation as the work context.
870     RDFVariable affiliation = lookupAffiliation(QContactDetail::ContextWork);
871
872     // Collect inserts for each regular detail.
873     foreach(const DetailMapping &detail, mappings()) {
874         // Store custom details via nao:Property.
875         if (detail.isCustomDetail()) {
876             insertCustomDetail(contact, detail);
877             continue;
878         }
879
880         // Don't save synthesized details.
881         if (detail.isSyntheticDetail()) {
882             continue;
883         }
884
885         const QVariantMap detailValues = detail.fieldValues();
886         const EntityHash entities = collectEntities(classes(), detail);
887         QStringList detailContexts = detail.contexts();
888         PredicateVariableHash objectCache;
889
890         const bool hasOtherContext =
891                 (detailContexts.isEmpty() ||
892                  detailContexts.removeAll(QContactDetail::ContextOther) > 0);
893
894         // Add the prepared work context's affiliation to object cache
895         // to make sure it will be used when an organization detail would be used.
896         objectCache.insert(PredicateChain() << nco::hasAffiliation::iri(), affiliation);
897
898         foreach(const QTrackerContactDetailField &field, detail.trackerFields()) {
899             if (field.hasSubTypes()) {
900                 // Subtypes are stored indirectly by assigning the proper resource class
901                 // to the RDF resource representing this field, or by selecting the matching
902                 // RDF property for storing the detail value.
903                 continue;
904             }
905
906             const QVariantMap::ConstIterator fieldValue = detailValues.find(field.name());
907             QVariant rdfValue;
908
909             if (fieldValue == detailValues.end() ||
910                 not field.makeValue(*fieldValue, rdfValue)) {
911                 // Could not compute the RDF value for this field.
912                 continue;
913             }
914
915             if (hasOtherContext) {
916                 insertDetailField(contactLocalId(), contact, detail, field,
917                                   rdfValue, entities, objectCache);
918             }
919
920             foreach(const QString &context, detailContexts) {
921                 RDFVariable contextAffiliation = lookupAffiliation(context);
922                 restrictAffiliation(contact, contextAffiliation, context);
923
924                 insertDetailField(contactLocalId(), contextAffiliation,
925                                   detail, field, rdfValue, entities, objectCache);
926             }
927         }
928     }
929
930     // Check if the prepared affiliation was used for binding contact details.
931     // In that case this prepare affiliation must be turned into a proper one.
932     foreach(const RDFStatement &statement, m_explicitInserts) {
933         if (statement.subject().metaIdentifier() == affiliation.metaIdentifier()) {
934             restrictAffiliation(contact, statement.subject(), QContactDetail::ContextWork);
935             break;
936         }
937     }
938
939     // Insert timestamps and such.
940     insert(contact, nie::contentLastModified::iri(), LiteralValue(lastModified()));
941     insert(contact, nie::contentCreated::iri(), LiteralValue(created()), hadCreated());
942     insert(contact, nco::contactUID::iri(), LiteralValue(guid()), hadGuid());
943
944     // Add prepared INSERT statements to the RDF update.
945     update.addInsertion(m_explicitInserts);
946
947     foreach(const RDFStatement &statement, m_implicitInserts) {
948         update.addInsertion(statement);
949     }
950 }
951
952 void
953 UpdateBuilder::deleteContactProperties(RDFUpdate &update) const
954 {
955     RDFVariable contact = schema().makeContactIri(contactLocalId());
956
957     RDFVariable domain(QString::fromLatin1("d"));
958     RDFVariable predicate(QString::fromLatin1("p"));
959     RDFVariable object(QString::fromLatin1("o"));
960
961     // Only remove properties of inherited classes in managed ontologies
962     domain.isMemberOf(iriVariableList(schema().inheritedClassIris()));
963
964     predicate.property<rdfs::domain>(domain);
965     predicate.notEqual(nco::contactLocalUID::iri());
966
967     // Remove all properties of Role, Contact and PersonContact domain,
968     // except for local id and globally unique id.
969     if (not hadGuid()) {
970         predicate.notEqual(nco::contactUID::iri());
971     }
972
973     // NB#177560 - skip removing nco::IMAddress for self Contact
974     if (isSelfContact()) {
975        predicate.notEqual(nco::hasIMAddress::iri());
976     }
977
978     update.addDeletion(contact, predicate, object);
979
980     // Remove timestamps when needed.
981     if (hadAccessed()) {
982         update.addDeletion(contact, nie::contentAccessed::iri());
983     }
984
985     if (hadCreated()) {
986         update.addDeletion(contact, nie::contentCreated::iri());
987     }
988
989     update.addDeletion(contact, nie::contentLastModified::iri());
990
991     // Remove tags
992     update.addDeletion(contact, nao::hasTag::iri());
993 }
994
995 void
996 UpdateBuilder::deleteCustomDetails(RDFUpdate &update) const
997 {
998     RDFVariable contact = schema().makeContactIri(contactLocalId());
999     RDFVariable customDetail(QString::fromLatin1("customDetail"));
1000     RDFVariable customField(QString::fromLatin1("customField"));
1001
1002     update.addDeletion(RDFStatementList() <<
1003                        RDFStatement(customField, rdf::type::iri(), nao::Property::iri()) <<
1004                        RDFStatement(customDetail, rdf::type::iri(), nao::Property::iri()) <<
1005                        RDFStatement(customDetail, rdf::type::iri(), rdfs::Resource::iri()) <<
1006                        RDFStatement(contact, nao::hasProperty::iri(), customDetail));
1007
1008     contact.property<nao::hasProperty>(customDetail);
1009     customDetail.property<nao::hasProperty>(customField);
1010 }
1011
1012 void
1013 deleteCustomValues(RDFUpdate &update, const RDFVariable &resource,
1014                    const QTrackerContactDetail &definition)
1015 {
1016     RDFVariableList customDetailFields;
1017
1018     foreach(const QTrackerContactDetailField &field, definition.fields()) {
1019         if (field.permitsCustomValues()) {
1020             customDetailFields += LiteralValue(field.name());
1021         }
1022     }
1023
1024
1025     if (not customDetailFields.isEmpty()) {
1026         RDFVariable customProperty = resource.property<nao::hasProperty>();
1027         update.addDeletion(customProperty, rdf::type::iri(), nao::Property::iri());
1028         customProperty.property<nao::propertyName>().isMemberOf(customDetailFields);
1029     }
1030 }
1031
1032 void
1033 UpdateBuilder::deleteAssociatedObjects(RDFUpdate &update) const
1034 {
1035     RDFVariable contact = schema().makeContactIri(contactLocalId());
1036
1037     // Remove all affiliation and organization properties in one go.
1038     // Initially we limited to nco:Role instead of rdf:Resource to only hit
1039     // "owned" properties, but this causes trouble with properties like
1040     // nco:fullname which for instance also implements dc:title.
1041     RDFVariable affiliation = makeAffiliationIri(contactLocalId());
1042     update.addDeletion(affiliation, rdf::type::iri(), rdfs::Resource::iri());
1043
1044     RDFVariable organization = makeOrganizationIri(contactLocalId());
1045     update.addDeletion(organization, rdf::type::iri(), rdfs::Resource::iri());
1046
1047     // Reset subtypes of associated entities
1048     foreach(const QTrackerContactDetail &definition, schema().details()) {
1049         const QTrackerContactDetailField *const subTypeField = definition.subTypeField();
1050         if (0 == subTypeField || not subTypeField->hasSubTypes()) {
1051             continue;
1052         }
1053
1054         const QTrackerContactDetailField *const subjectField = definition.subjectField();
1055
1056         if (0 == subjectField) {
1057             // There is no subject field for this detail.
1058             continue;
1059         }
1060
1061         const PropertyInfoList &subjectPropertyChain = subjectField->propertyChain();
1062
1063         if (subjectPropertyChain.count() < 2) {
1064             // There must be at least the property creating the association
1065             // and the property storing the value.
1066             continue;
1067         }
1068
1069         const PropertyInfoBase &subjectProperty = subjectPropertyChain[1];
1070         const QTrackerContactSubject::Scheme scheme = subjectProperty.domainScheme();
1071
1072         if (QTrackerContactSubject::isContentScheme(scheme)) {
1073             // delete associated object by its content IRI
1074             typedef QPair<QUrl, QUrl> IriPair;
1075             QList<IriPair> resourceIris;
1076
1077             foreach(const DetailMapping &mapping, mappings()) {
1078                 if (mapping.definitionName() != definition.name()) {
1079                     continue;
1080                 }
1081
1082                 const QVariant value = mapping.fieldValue(subjectField->name());
1083
1084                 if (value.isNull()) {
1085                     continue;
1086                 }
1087
1088                 resourceIris += qMakePair(makeIri(scheme, QVariantList() << value),
1089                                           subjectProperty.domainIri());
1090             }
1091
1092             foreach(const IriPair &iris, resourceIris) {
1093                 deleteCustomValues(update, iris.first, definition);
1094             }
1095
1096             foreach(const IriPair &iris, resourceIris) {
1097                 update.addDeletion(iris.first, rdf::type::iri(), iris.second);
1098             }
1099         } else {
1100             // no content IRI known, delete via association
1101             const PropertyInfoBase &associatingProperty = subjectPropertyChain[0];
1102             RDFVariable resource = associatingProperty.bindProperty(contact);
1103
1104             deleteCustomValues(update, resource.deepCopy(), definition);
1105             update.addDeletion(resource, rdf::type::iri(), rdfs::Resource::iri());
1106         }
1107     }
1108 }
1109
1110 void
1111 UpdateBuilder::collectDeletions(RDFUpdate &update) const
1112 {
1113     deleteContactProperties(update);
1114     deleteCustomDetails(update);
1115     deleteAssociatedObjects(update);
1116 }
1117
1118 QUrl
1119 UpdateBuilder::makeIri(QTrackerContactSubject::Scheme scheme, const QVariantList &values) const
1120 {
1121     return QTrackerContactSubject::makeIri(scheme, contactLocalId(), values);
1122 }
1123
1124 RDFVariable &
1125 UpdateBuilder::lookupAffiliation(const QString &context)
1126 {
1127     AffiliationMap::Iterator affilination = m_affiliations.find(context);
1128
1129     // create nco::Affiliation entity when needed
1130     if (affilination == m_affiliations.end()) {
1131         const QString baseName = QTrackerContactQueryBuilder::name(QLatin1String("Affiliation"), context);
1132         affilination = m_affiliations.insert(context, RDFVariable(makeUniqueName(baseName)));
1133    }
1134
1135     return *affilination;
1136 }
1137
1138 void
1139 QTrackerContactSaveRequest::normalizeCurrentContact()
1140 {
1141     QContact &contact = m_contacts[m_contactOffset];
1142
1143     foreach(const QTrackerContactDetail &detail, engine()->schema(contact.type()).details()) {
1144         if (not detail.isUnique()) {
1145             continue;
1146         }
1147
1148         QList<QContactDetail> contactDetails(contact.details(detail.name()));
1149
1150         if (contactDetails.count() < 2) {
1151             continue;
1152         }
1153
1154         qctWarn(QString::fromLatin1("Dropping odd details for contact %1: "
1155                                     "%2 detail must be unique").
1156                 arg(m_contactOffset).arg(detail.name()));
1157
1158         for(int i = 1; i < contactDetails.count(); ++i) {
1159             contact.removeDetail(&contactDetails[i]);
1160         }
1161     }
1162 }
1163
1164 static QString
1165 makeAvatarCacheFileName(const QImage& image)
1166 {
1167     // FIXME: H-Contacts needs to fix loading of images from URI
1168     // when not inside their own cache directory.
1169
1170     const QDir avatarCacheDir = qctAvatarCacheDir();
1171
1172     // Create avatar cache directory when needed.
1173
1174     if (not avatarCacheDir.mkpath(QLatin1String("."))) {
1175         qctWarn(QString::fromLatin1("Cannot create avatar cache folder %1: %2").
1176                 arg(avatarCacheDir.path(), strerror(errno)));
1177         return QString();
1178     }
1179
1180     // Create filename from cryptographic hash of the pixel data. Probability that two
1181     // different pictures on the same device have the same hash code, is significantly
1182     // smaller than probability that we mess up because of miscalculations caused by
1183     // electrostatics and flipping bits. That's why we can apply this cheap trick.
1184
1185     const QByteArray pixels(reinterpret_cast<const char *>(image.constBits()), image.byteCount());
1186     const QString pixelHash = QCryptographicHash::hash(pixels, QCryptographicHash::Sha1).toHex();
1187
1188     const QString fileName = QString::fromLatin1("%1.png").arg(pixelHash);
1189     return avatarCacheDir.absoluteFilePath(fileName);
1190 }
1191
1192 static QSet<QString>
1193 findAccountDetailUris(const QContact &contact)
1194 {
1195     QSet<QString> onlineAccountUris;
1196
1197     foreach(const QContactOnlineAccount &detail, contact.details<QContactOnlineAccount>()) {
1198         onlineAccountUris += detail.detailUri();
1199     }
1200
1201     return onlineAccountUris;
1202 }
1203
1204 static bool
1205 isLinkedDetail(const QContactDetail &detail, const QSet<QString> &linkedDetailUris)
1206 {
1207     foreach(const QString &uri, detail.linkedDetailUris()) {
1208         if (linkedDetailUris.contains(uri)) {
1209             return true;
1210         }
1211     }
1212
1213     return false;
1214 }
1215
1216 bool
1217 QTrackerContactSaveRequest::normalizeAvatars()
1218 {
1219     QContact &contact = m_contacts[m_contactOffset];
1220     const QSet<QString> accountDetailUris = findAccountDetailUris(contact);
1221
1222     // Find user supplied thumbnail of the contact itself.
1223     QContactThumbnail defaultThumbnail;
1224
1225     foreach(const QContactThumbnail &thumbnail, contact.details<QContactThumbnail>()) {
1226         if (not isLinkedDetail(thumbnail, accountDetailUris)) {
1227             defaultThumbnail = thumbnail;
1228             break;
1229         }
1230     }
1231
1232     if (defaultThumbnail.isEmpty()) {
1233         return true;
1234     }
1235
1236     // Find default avatar of the contact itself.
1237     QContactAvatar defaultAvatar;
1238
1239     foreach(const QContactAvatar &avatar, contact.details<QContactAvatar>()) {
1240         if (not isLinkedDetail(avatar, accountDetailUris)) {
1241             defaultAvatar = avatar;
1242             break;
1243         }
1244     }
1245
1246     // Get avatar thumbnail from detail and scale down when needed.
1247     const QSize avatarSize = QTrackerContactSettings().avatarSize();
1248     QImage thumbnailImage = defaultThumbnail.thumbnail();
1249
1250     if (thumbnailImage.width() > avatarSize.width() ||
1251         thumbnailImage.height() > avatarSize.height()) {
1252         thumbnailImage = thumbnailImage.scaled(avatarSize, Qt::KeepAspectRatio);
1253     }
1254
1255     // Save the avatar image when neeed. The cached file name is based on
1256     // a cryptographic hash of the thumbnail's pixels. So if there already
1257     // exists a image file with the calculated name, there is an incredibly
1258     // high probability the existing file is equal to this current avatar.
1259     // We prefer to not write the file in this case for better performance
1260     // and for better lifetime of the storage medium.
1261     const QString avatarCacheFileName = makeAvatarCacheFileName(thumbnailImage);
1262     bool avatarWritten = true;
1263
1264     if (avatarCacheFileName.isEmpty()) {
1265         avatarWritten = false; // failed to build file name
1266     } else if (not QFile::exists(avatarCacheFileName)) {
1267         QImageWriter writer(avatarCacheFileName);
1268
1269         avatarWritten = writer.write(thumbnailImage);
1270
1271         if (not avatarWritten) {
1272             qctWarn(QString::fromLatin1("Cannot save avatar thumbnail for contact %1: %2").
1273                     arg(m_contactOffset).arg(writer.errorString()));
1274         }
1275     }
1276
1277     if (avatarWritten) {
1278         // everything went find - update avatar detail and proceed
1279         defaultAvatar.setImageUrl(QUrl::fromLocalFile(avatarCacheFileName));
1280         contact.saveDetail(&defaultAvatar);
1281     } else if (not defaultAvatar.isEmpty()) {
1282         // failed to save avatar picture - remove avatar detail
1283         contact.removeDetail(&defaultAvatar);
1284     }
1285
1286     return avatarWritten;
1287 }
1288
1289 RDFUpdate
1290 QTrackerContactSaveRequest::buildQuery(QContact &contact)
1291 {
1292     UpdateBuilder builder(this, contact, timestamp());
1293
1294     // Create local contact id when needed
1295     QContactId id = contact.id();
1296     id.setLocalId(builder.contactLocalId());
1297     id.setManagerUri(engine()->managerUri());
1298     contact.setId(id);
1299
1300     // Build the update query
1301     RDFUpdate update;
1302
1303     builder.collectDeletions(update);
1304     builder.collectInsertions(update);
1305
1306     return update;
1307 }
1308
1309 ///////////////////////////////////////////////////////////////////////////////////////////////////
1310
1311 void
1312 QTrackerContactSaveRequest::saveContact()
1313 {
1314     if (engine()->hasDebugFlag(QContactTrackerEngine::ShowNotes)) {
1315         qDebug()
1316                 << metaObject()->className() << m_stopWatch.elapsed()
1317                 << ": contact" << m_contactOffset << "- updating";
1318     }
1319
1320     // drop odd details
1321     normalizeCurrentContact();
1322
1323     // normalize and save avatar
1324     if (not normalizeAvatars()) {
1325         return;
1326     }
1327
1328     // build the update query and run it
1329     const RDFUpdate query = buildQuery(m_contacts[m_contactOffset]);
1330     const QString queryString = ::BackEnds::Tracker::tracker()->rawQueryString(query);
1331
1332     if (engine()->hasDebugFlag(QContactTrackerEngine::ShowUpdates)) {
1333         qDebug() << queryString;
1334     }
1335
1336     QDBusPendingCall updateCall = QctTracker::Resources::
1337             batchSparqlUpdate(m_trackerInstance, queryString,
1338                               engine()->trackerTimeout());
1339
1340     QTrackerContactSaveRequestTask *pendingUpdate =
1341             new QTrackerContactSaveRequestTask(updateCall, m_contactOffset, this);
1342
1343     connect(pendingUpdate, SIGNAL(failure(int,QString)), SLOT(failure(int,QString)));
1344     connect(pendingUpdate, SIGNAL(success(int)), SLOT(success(int)));
1345     m_pendingTasks.insert(m_contactOffset, pendingUpdate);
1346
1347     if (++m_batchSize >= engine()->batchSize()) {
1348         commit();
1349     }
1350 }
1351
1352 void
1353 QTrackerContactSaveRequest::proceed()
1354 {
1355     // Too many updates are running still, let's abort.
1356     // We'll get called again when next pending update is commited.
1357     if (m_pendingTasks.count() >= engine()->concurrencyLevel() / 2 &&
1358         m_pendingTasks.count() > 0) {
1359         return;
1360     }
1361
1362     while (m_contactOffset < m_contacts.count()) {
1363         // Now we have queued too many updates, let's abort.
1364         // We'll get called again when next pending update is commited.
1365         if (m_pendingTasks.count() >= engine()->concurrencyLevel()) {
1366             return;
1367         }
1368
1369         saveContact();
1370         m_contactOffset++;
1371     }
1372
1373     if (hasPendingUpdates()) {
1374         commit(); // We left the saveContact() loop, and now must commit the queued updates.
1375         return;
1376     }
1377
1378     // We left the saveContact() loop, and don't have any pending updates or
1379     // pending commits. This shows the save request was finished. Let's report this.
1380     if (not hasPendingCommit()) {
1381         if (m_errorMap.isEmpty()) {
1382             m_effectiveError = QContactManager::NoError;
1383         }
1384
1385         emitResult(m_effectiveError);
1386     }
1387 }
1388
1389 void
1390 QTrackerContactSaveRequest::commit()
1391 {
1392     // Don't do anything if another commit is running still.
1393     if (hasPendingCommit()) {
1394         return;
1395     }
1396
1397     // Dump some debug information when requested.
1398     if (engine()->hasDebugFlag(QContactTrackerEngine::ShowNotes)) {
1399         qDebug()
1400                 << metaObject()->className() << m_stopWatch.elapsed()
1401                 << "- commiting" << m_batchSize << "pending updates";
1402     }
1403
1404     // Tell tracker to commit queued updates.
1405     QDBusPendingCall commitCall = QctTracker::Resources::
1406             batchCommit(m_trackerInstance, engine()->trackerTimeout());
1407
1408     m_commit.reset(new QDBusPendingCallWatcher(commitCall));
1409
1410     connect(m_commit.data(),
1411             SIGNAL(finished(QDBusPendingCallWatcher*)),
1412             SLOT(commitFinished()));
1413
1414     m_batchSize = 0;
1415 }
1416
1417 void
1418 QTrackerContactSaveRequest::commitFinished()
1419 {
1420     if (m_commit->isError()) {
1421         // Seems we don't have to do anything for for this error except for printing
1422         // it to console: We won't get a success signal for the failed update, and
1423         // therefore the failed contact should stay in m_errorMap[] causing proper
1424         // error reporting.
1425         qctWarn(QString::fromLatin1("Commit failed: %1").arg(m_commit->error().message()));
1426     }
1427
1428
1429     m_commit.reset(); // Mark this commit as done.
1430     proceed(); // Check if other updates are pending, or if more contacts must be written.
1431 }
1432
1433 void
1434 QTrackerContactSaveRequest::success(int offset)
1435 {
1436     if (engine()->hasDebugFlag(QContactTrackerEngine::ShowNotes)) {
1437         qDebug()
1438                 << metaObject()->className() << m_stopWatch.elapsed()
1439                 << ": contact" << offset << " - update succeded";
1440     }
1441
1442     delete m_pendingTasks.take(offset); // Mark this update as done.
1443     m_errorMap.remove(offset); // Mark this update's contact as successfully written.
1444     proceed(); // Check if other updates are pending, or if more contacts must be written.
1445 }
1446
1447 void
1448 QTrackerContactSaveRequest::failure(int offset, QString const &message)
1449 {
1450     qctWarn(QString::fromLatin1("Failed to update contact %1: %2").arg(offset).arg(message));
1451
1452     delete m_pendingTasks.take(offset); // Mark this update as done.
1453     proceed(); // Check if other updates are pending, or if more contacts must be written.
1454
1455     // Notice: We don't remove the update's contact from m_errorMap[]
1456     // which results in proper error reporting.
1457 }
1458
1459 ///////////////////////////////////////////////////////////////////////////////////////////////////
1460
1461 bool
1462 QTrackerContactSaveRequest::hasPendingUpdates() const
1463 {
1464     return not m_pendingTasks.isEmpty();
1465 }
1466
1467 bool
1468 QTrackerContactSaveRequest::hasPendingCommit() const
1469 {
1470     return not m_commit.isNull();
1471 }
1472
1473 ///////////////////////////////////////////////////////////////////////////////////////////////////
1474
1475 bool
1476 QTrackerContactSaveRequest::start()
1477 {
1478     if (engine()->hasDebugFlag(QContactTrackerEngine::ShowNotes)) {
1479         qDebug()
1480                 << metaObject()->className() << 0
1481                 << ": number of contacts:" << m_request->contacts().count();
1482     }
1483
1484     m_stopWatch.start();
1485
1486     // prepare error map with default values
1487     for(int i = 0; i < m_contacts.count(); ++i) {
1488         m_errorMap.insert(i, QContactManager::UnspecifiedError);
1489     }
1490
1491     // find tracker instance
1492     m_trackerInstance = QctTracker::instance();
1493
1494     if (m_trackerInstance.isEmpty()) {
1495         qctWarn(QString::fromLatin1("tracker service not found: %1").arg(QctTracker::lastError));
1496         return false;
1497     }
1498
1499     // start operation
1500     m_contactOffset = 0;
1501     QTimer::singleShot(0, this, SLOT(proceed()));
1502
1503     return true;
1504 }
1505
1506 void
1507 QTrackerContactSaveRequest::emitResult(QContactManager::Error error)
1508 {
1509     if (engine()->hasDebugFlag(QContactTrackerEngine::ShowTiming)) {
1510         qDebug()
1511                 << metaObject()->className()  << m_stopWatch.elapsed()
1512                 << ": reporting result" << error;
1513     }
1514
1515     engine()->updateContactSaveRequest(m_request, m_contacts, error, m_errorMap,
1516                                       QContactAbstractRequest::FinishedState);
1517 }
1518
1519 ///////////////////////////////////////////////////////////////////////////////////////////////////
1520
1521 #include "contactsaverequest.moc"