Changes: Rename bindSort() to bindSortOrders()
[qtcontacts-tracker:hasselmms-qtcontacts-tracker.git] / src / engine / abstractcontactfetchrequest.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 "abstractcontactfetchrequest.h"
26
27 #include "engine.h"
28
29 #include <dao/contactdetail.h>
30 #include <dao/contactdetailschema.h>
31 #include <dao/scalarquerybuilder.h>
32 #include <dao/subject.h>
33 #include <dao/support.h>
34 #include <lib/constants.h>
35 #include <lib/logger.h>
36 #include <lib/presenceutils.h>
37 #include <lib/requestextensions.h>
38 #include <lib/resourcecache.h>
39 #include <lib/sparqlconnectionmanager.h>
40
41 #include <QtSparql>
42
43 ///////////////////////////////////////////////////////////////////////////////////////////////////
44
45 CUBI_USE_NAMESPACE
46 CUBI_USE_NAMESPACE_RESOURCES
47
48 ///////////////////////////////////////////////////////////////////////////////////////////////////
49
50 class QTrackerAbstractContactFetchRequest::DetailContext
51 {
52 public:
53     explicit DetailContext(const QTrackerContactDetail &definition,
54                            int firstColumn, int lastColumn)
55         : m_definition(definition)
56         , m_firstColumn(firstColumn)
57         , m_lastColumn(lastColumn)
58     {
59     }
60
61     const QTrackerContactDetail & definition() const { return m_definition; }
62     int firstColumn() const { return m_firstColumn; }
63     int lastColumn() const { return m_lastColumn; }
64
65 private:
66     QTrackerContactDetail   m_definition;
67     int                     m_firstColumn;
68     int                     m_lastColumn;
69 };
70
71 ///////////////////////////////////////////////////////////////////////////////////////////////////
72
73 class QTrackerAbstractContactFetchRequest::QueryContext
74 {
75 public:
76     explicit QueryContext(const QTrackerContactDetailSchema &schema)
77         : result(0)
78         , customDetailColumn(-1)
79         , hasMemberRelationshipColumn(-1)
80         , fetchAllDetails(false)
81         , sorted(false)
82         , m_schema(schema)
83     {
84     }
85
86 public: // attributes
87     const QTrackerContactDetailSchema & schema() const { return m_schema; }
88     const QString & contactType() const { return m_schema.contactType(); }
89
90 public: // fields
91     Select query;
92     QSparqlResult *result;
93
94     QList<DetailContext> details;
95     QSet<QString> definitionHints;
96     QSet<QString> customDetailHints;
97     QList<QContactLocalId> contactIds;
98     int customDetailColumn;
99     int hasMemberRelationshipColumn;
100
101     bool fetchAllDetails : 1;
102     bool sorted : 1;
103
104 private: // fields
105     QTrackerContactDetailSchema m_schema;
106 };
107
108 ///////////////////////////////////////////////////////////////////////////////////////////////////
109
110 QTrackerAbstractContactFetchRequest::QTrackerAbstractContactFetchRequest(QContactAbstractRequest *request,
111                                                                          const QContactFilter &filter,
112                                                                          const QContactFetchHint &fetchHint,
113                                                                          const QList<QContactSortOrder> &sorting,
114                                                                          QContactTrackerEngine *engine,
115                                                                          QObject *parent)
116     : QTrackerAbstractRequest(engine, parent)
117     , m_filter(filter)
118     , m_fetchHint(engine->normalizedFetchHint(fetchHint,
119                                               QctRequestExtensions::get(request)->nameOrder()))
120     , m_nameOrder(QctRequestExtensions::get(request)->nameOrder())
121     , m_sorting(sorting)
122 {
123 }
124
125 QTrackerAbstractContactFetchRequest::~QTrackerAbstractContactFetchRequest()
126 {
127 }
128
129 /// returns a query for the contact iri, the nco::contactLocalUID, the context iri, the rdfs::label of the context
130 Select
131 QTrackerAbstractContactFetchRequest::baseQuery(const QTrackerScalarContactQueryBuilder &queryBuilder) const
132 {
133     Variable contact(queryBuilder.contact());
134     Variable context(queryBuilder.context());
135
136     Select query;
137
138     query.addProjection(contact);
139     query.addProjection(Functions::trackerId.apply(contact));
140     query.addProjection(context);
141     query.addProjection(rdfs::label::function().apply(context));
142
143     foreach(const QString &classIri, queryBuilder.schema().contactClassIris()) {
144         query.addRestriction(contact, rdf::type::resource(), ResourceValue(classIri));
145     }
146
147     PatternGroup contextPattern;
148     contextPattern.setOptional(true);
149     contextPattern.addPattern(contact, nco::hasAffiliation::resource(), context);
150     query.addRestriction(contextPattern);
151
152     return query;
153 }
154
155 static QContactManager::Error
156 bindFilters(QTrackerScalarContactQueryBuilder &queryBuilder,
157             const QContactFilter &filter, Select &select)
158 {
159     Filter result;
160
161     const QContactManager::Error error = queryBuilder.bindFilter(filter, result);
162     select.setFilter(result);
163
164     return error;
165 }
166
167 /// Returns the @p rawValueString with the graphIri removed if present.
168 /// Sets @p isOtherGraph to @c true if the graphIri is not the one of qct and not emoty, @c false otherwise.
169 QString
170 QTrackerAbstractContactFetchRequest::fieldStringWithStrippedGraph(const QTrackerContactDetailField &field,
171                                                                   const QString &rawValueString, bool &isOtherGraph) const
172 {
173     if (not field.hasOwner()) {
174         return rawValueString;
175     }
176
177     QString string = rawValueString;
178     const int s = string.indexOf(QTrackerScalarContactQueryBuilder::graphSeparator());
179     if (s < 0) {
180         qctWarn(QString::fromLatin1("Could not find graphIri added for field %1: %2").
181                 arg(field.name(), string));
182         isOtherGraph = false;
183     } else {
184         const QString fieldGraphIri = string.mid(s+1);
185         string.truncate(s);
186
187         isOtherGraph =
188             (not fieldGraphIri.isEmpty()) &&
189             (QtContactsTrackerDefaultGraphIri != fieldGraphIri);
190
191         if (engine()->hasDebugFlag(QContactTrackerEngine::ShowNotes)) {
192             if (isOtherGraph) {
193                 qDebug() << "Read field from other graph:" << field.name() << string << fieldGraphIri;
194             }
195         }
196     }
197     return string;
198 }
199
200 /// Returns the @p rawValueString splitted into stringlist with the graphIris removed if present.
201 /// Sets @p isOtherGraph to @c true if any graphIri is not the one of qct and not emoty, @c false otherwise.
202 QStringList
203 QTrackerAbstractContactFetchRequest::fieldStringListWithStrippedGraph(const QTrackerContactDetailField &field,
204                                                                       const QString &rawValueString,
205                                                                       bool &isOtherGraph,
206                                                                       bool cleanList) const
207 {
208     QStringList list = rawValueString.split(QTrackerScalarContactQueryBuilder::listSeparator());
209     if (cleanList) {
210         list.removeAll(QString());
211         list.removeDuplicates();
212     }
213
214     if (not field.hasOwner()) {
215         return list;
216     }
217
218     isOtherGraph = false;
219     for (int i = 0; i<list.size(); ++i) {
220         QString &string = list[i];
221         const int s = string.indexOf(QTrackerScalarContactQueryBuilder::graphSeparator());
222         if (s < 0) {
223             qctWarn(QString::fromLatin1("Could not find graphIri added for field %1: %2").
224                     arg(field.name(), string));
225         } else {
226             const QString fieldGraphIri = string.mid(s+1);
227             string.truncate(s);
228
229             const bool isItemFromOtherGraph =
230                 (not fieldGraphIri.isEmpty()) &&
231                 (QtContactsTrackerDefaultGraphIri != fieldGraphIri);
232             isOtherGraph |= isItemFromOtherGraph;
233             if (engine()->hasDebugFlag(QContactTrackerEngine::ShowNotes)) {
234                 if (isItemFromOtherGraph) {
235                     qDebug() << "Read field from other graph:" << field.name() << string << fieldGraphIri;
236                 }
237             }
238         }
239     }
240     return list;
241 }
242
243
244 QContactManager::Error
245 QTrackerAbstractContactFetchRequest::bindDetails(QueryContext &context) const
246 {
247     QTrackerScalarContactQueryBuilder queryBuilder(context.schema(), engine()->managerUri());
248
249     context.query = baseQuery(queryBuilder);
250
251     foreach(const QTrackerContactDetail &detail, context.schema().details()) {
252         if (detail.isSynthesized()) {
253             continue;
254         }
255
256         // skip details which are not needed according to the fetch hints
257         if (not context.definitionHints.contains(detail.name()) &&
258                 not context.definitionHints.isEmpty()) {
259             continue;
260         }
261
262         if (detail.fields().isEmpty()) {
263             if (engine()->hasDebugFlag(QContactTrackerEngine::ShowNotes)) {
264                 qctWarn(QString::fromLatin1("Not implemented yet, skipping %1 detail").
265                         arg(detail.name()));
266             }
267
268             continue;
269         }
270
271         // bind this detail to the proper query
272         const int firstColumn = context.query.projections().count();
273         const QContactManager::Error error = queryBuilder.bindFields(detail, context.query);
274
275         // verify results
276         if (error != QContactManager::NoError) {
277             context.query = Select();
278             return error;
279         }
280
281         if (context.query.isEmpty()) {
282             qctWarn("No queries built");
283             return QContactManager::UnspecifiedError;
284         }
285
286         // store this detail's context
287         const int lastColumn = context.query.projections().count();
288         context.details += DetailContext(detail, firstColumn, lastColumn);
289     }
290
291     // bind the filters
292     const QContactManager::Error filterError = bindFilters(queryBuilder, m_filter, context.query);
293     if (filterError != QContactManager::NoError) {
294         return filterError;
295     }
296
297     // check if we can sort results in Tracker
298     // return value is ignored, if sorting fails we'll fallback to in-memory sorting
299     bindSorting(queryBuilder, context);
300
301     // Create almost empty base query if actually no unique details where requested by the
302     // fetch hints. This is needed to let fetchBaseModel() populate the contact cache, and more
303     // importantly it is needed to avoid bogus "DoesNotExistError" errors if existing contacts
304     // are requested via local id filter, but those contacts don't have any of the requested
305     // details.
306     if (context.query.isEmpty()) {
307         QTrackerScalarContactQueryBuilder queryBuilder(context.schema(), engine()->managerUri());
308
309         context.query = baseQuery(queryBuilder);
310         const QContactManager::Error error = bindFilters(queryBuilder, m_filter, context.query);
311
312         if (QContactManager::NoError != error) {
313             context.query = Select();
314             return error;
315         }
316
317         bindSorting(queryBuilder, context);
318     }
319
320     return QContactManager::NoError;
321 }
322
323 static bool
324 checkRelationshipTypesHint(const QStringList &relationshipTypes)
325 {
326     if (relationshipTypes.isEmpty()) {
327         return true; // no hint specified -> fetch all relationships
328     }
329
330     QSet<QString> unsupportedTypes;
331     bool fetchRelationships = false;
332
333     foreach (const QString &type, relationshipTypes) {
334         if (type == QContactRelationship::HasMember) {
335             // Do not abort, but check the other types as well for complete feedback.
336             fetchRelationships = true;
337         } else {
338             // Unsupported types are ignored, do not result in error
339             unsupportedTypes += type;
340         }
341     }
342
343     if (not unsupportedTypes.isEmpty()) {
344         qctWarn(QString::fromLatin1("Only HasMember relationship is supported, "
345                                     "but got %1 in contact fetch hint.").
346                 arg(QStringList(unsupportedTypes.toList()).join(QLatin1String(", "))));
347     }
348
349     return fetchRelationships;
350 }
351
352 QContactManager::Error
353 QTrackerAbstractContactFetchRequest::buildQuery(QueryContext &context) const
354 {
355     if (not m_fetchHint.detailDefinitionsHint().isEmpty()) {
356         context.definitionHints = m_fetchHint.detailDefinitionsHint().toSet();
357
358         if (engine()->hasDebugFlag(QContactTrackerEngine::ShowNotes)) {
359             qDebug() << "explicit definition hints:" << m_fetchHint.detailDefinitionsHint();
360         }
361
362         // make sure we can synthesized all requested details
363         foreach(const QString &name, m_fetchHint.detailDefinitionsHint()) {
364             const QTrackerContactDetail *const detail = context.schema().detail(name);
365
366             if (0 != detail) {
367                 context.definitionHints += detail->dependencies();
368             } else if (not context.schema().isSyntheticDetail(name)) {
369                 context.customDetailHints += name;
370             }
371         }
372
373         // make sure we fetch everything needed for sorting
374         foreach(const QContactSortOrder &order, m_sorting) {
375             const QString name = order.detailDefinitionName();
376             const QTrackerContactDetail *const detail = context.schema().detail(name);
377
378             if (0 != detail) {
379                 context.definitionHints += name;
380             } else if (not context.schema().isSyntheticDetail(name)) {
381                 context.customDetailHints += name;
382             }
383         }
384
385         if (engine()->hasDebugFlag(QContactTrackerEngine::ShowNotes)) {
386             qDebug() << "final definition hints:" << context.definitionHints;
387             qDebug() << "custom detail hints:" << context.customDetailHints;
388         }
389     } else {
390         if (engine()->hasDebugFlag(QContactTrackerEngine::ShowNotes)) {
391             qDebug() << "fetching all details";
392         }
393
394         context.fetchAllDetails = true;
395         context.definitionHints.clear();
396     }
397
398     const QContactManager::Error error = bindDetails(context);
399
400     if (QContactManager::NoError != error) {
401         return error;
402     }
403
404     // Build custom detail query when needed
405     if (not context.customDetailHints.isEmpty() || context.fetchAllDetails) {
406         QTrackerScalarContactQueryBuilder queryBuilder(context.schema(), engine()->managerUri());
407         context.customDetailColumn = context.query.projections().count();
408         queryBuilder.bindCustomDetails(context.query, context.customDetailHints);
409     }
410
411     // Build relationships query if asked to (only HasMember supported)
412     if (not m_fetchHint.optimizationHints().testFlag(QContactFetchHint::NoRelationships)) {
413         if (checkRelationshipTypesHint(m_fetchHint.relationshipTypesHint())) {
414             context.hasMemberRelationshipColumn = context.query.projections().count();
415
416             if (engine()->hasDebugFlag(QContactTrackerEngine::ShowNotes)) {
417                 qDebug() << "fetching HasMember relationships";
418             }
419
420             QTrackerScalarContactQueryBuilder queryBuilder(context.schema(), engine()->managerUri());
421             queryBuilder.bindHasMemberRelationships(context.query);
422         }
423     }
424
425     if (context.query.isEmpty()) {
426         qctWarn("No queries built");
427         return QContactManager::UnspecifiedError;
428     }
429
430     return QContactManager::NoError;
431 }
432
433 void
434 QTrackerAbstractContactFetchRequest::bindSorting(QTrackerScalarContactQueryBuilder &queryBuilder,
435                                                  QTrackerAbstractContactFetchRequest::QueryContext &context) const
436 {
437     QList<OrderComparator> orderBy;
438     const QContactManager::Error error = queryBuilder.bindSortOrders(m_sorting, orderBy);
439
440     // No error forwarding needed, context.sorted is enough information
441     if (error != QContactManager::NoError) {
442         return;
443     }
444
445     context.query.setOrderBy(orderBy);
446     context.sorted = true;
447 }
448
449 Select
450 QTrackerAbstractContactFetchRequest::query(const QString &contactType,
451                                            QContactManager::Error &error) const
452 {
453     QTrackerContactDetailSchemaMap::ConstIterator schema = engine()->schemas().find(contactType);
454
455     if (schema == engine()->schemas().constEnd()) {
456         error = QContactManager::BadArgumentError;
457         return Select();
458     }
459
460     QueryContext context(engine()->schema(contactType));
461     error = buildQuery(context);
462     return context.query;
463 }
464
465 /// Returns a list of integers created from the strings in @p stringList.
466 /// The integers are in the order of the string, if a string could not be converted,
467 /// the corresponding integer is @c 0.
468 static QList<int>
469 toIntList(const QStringList &stringList)
470 {
471     QList<int> intList;
472
473     foreach(const QString &element, stringList) {
474         intList.append(element.toInt());
475     }
476
477     return intList;
478 }
479
480 /// Returns a list of integers created from the content of @p string.
481 /// The string is separated using @p separator (defaults to QTrackerScalarContactQueryBuilder::listSeparator()),
482 /// the integers are in the order of the separated substrings.
483 /// If a substring could not be converted, the corresponding integer is @c 0.
484 static QList<int>
485 toIntList(const QString &string,
486           const QChar separator = QTrackerScalarContactQueryBuilder::listSeparator())
487 {
488     return toIntList(string.split(separator));
489 }
490
491 /// returns the subtype(s) for the given field as a QVariant.
492 /// Uses the default subtype(s) if the passed @p subTypes is empty.
493 static QVariant
494 fetchSubTypes(const QTrackerContactDetailField &field,
495               QSet<QString> subTypes)
496 {
497     // apply default value if no subtypes could be read
498     if (subTypes.isEmpty() && field.hasDefaultValue()) {
499         switch(field.defaultValue().type()) {
500         case QVariant::String:
501             subTypes.insert(field.defaultValue().toString());
502             break;
503         case QVariant::StringList:
504             subTypes = field.defaultValue().toStringList().toSet();
505             break;
506         default:
507             qctWarn(QString::fromLatin1("Invalid type %1 for subtype field %2").
508                     arg(QLatin1String(field.defaultValue().typeName()), field.name()));
509             break;
510         }
511     }
512
513     // set field if any subtypes could be identified
514     if (not subTypes.isEmpty()) {
515         switch(field.dataType()) {
516         case QVariant::String:
517             // TODO: clone this detail multiple times when |subTypes| > 1
518             return *subTypes.begin();
519             break;
520
521         case QVariant::StringList:
522             return QStringList(subTypes.toList());
523             break;
524
525         default:
526             qctWarn(QString::fromLatin1("Invalid type %1 for subtype field %2").
527                     arg(QLatin1String(QVariant::typeToName(field.dataType())), field.name()));
528             break;
529         }
530     }
531
532     return QVariant();
533 }
534
535 static int
536 trackerId(const ResourceInfo &resource)
537 {
538     return QctResourceCache::instance().trackerId(resource.iri());
539 }
540
541 /// Returns the subtypes encoded in @p rawValueString as string or stringlist in a QVariant.
542 /// Returns the default subtype(s) if there is no known subtype in @p rawValueString.
543 static QVariant
544 fetchSubTypesClasses(const QTrackerContactDetailField &field,
545                      const QString &rawValueString)
546 {
547     const QList<int> fetchedSubTypes = toIntList(rawValueString);
548     QSet<QString> subTypes;
549
550     foreach(const ClassInfoBase &ci, field.subTypeClasses()) {
551         if (fetchedSubTypes.contains(trackerId(ci))) {
552             subTypes.insert(ci.text());
553         }
554     }
555
556     return fetchSubTypes(field, subTypes);
557 }
558
559 /// Returns the list of details, with each detail having all subtypes collected and set.
560 /// If there was no subtype for a detail, it was set the default subtype(s).
561 static QList<QContactDetail>
562 unifyPropertySubTypes(const QMultiHash<QString, QContactDetail> &details,
563                       const QTrackerContactDetailField &subTypeField)
564 {
565     QList<QContactDetail> results;
566     QStringList subTypes = details.uniqueKeys();
567     subTypes.removeOne(QString());
568
569     foreach(QContactDetail detail, details.values(QString())) {
570         QSet<QString> detailSubTypes;
571
572         foreach(const QString &subType, subTypes) {
573             if(details.values(subType).contains(detail)) {
574                 detailSubTypes.insert(subType);
575             }
576         }
577
578         detail.setValue(subTypeField.name(), fetchSubTypes(subTypeField, detailSubTypes));
579
580         results.append(detail);
581     }
582
583     return results;
584 }
585
586 QVariant
587 QTrackerAbstractContactFetchRequest::fetchInstances(const QTrackerContactDetailField &field,
588                                                     const QString &rawValueString, bool &isReadOnly) const
589 {
590     if (rawValueString.isEmpty()) {
591         return QVariant();
592     }
593
594     // apply string list when requested
595     if (QVariant::StringList == field.dataType()) {
596         const QStringList rawValueList = fieldStringListWithStrippedGraph(field, rawValueString, isReadOnly);
597         const QList<int> fetchedInstances = toIntList(rawValueList);
598
599         QStringList instances;
600
601         foreach(const InstanceInfoBase &ii, field.allowableInstances()) {
602             if (fetchedInstances.contains(trackerId(ii))) {
603                 instances.append(ii.text());
604             }
605         }
606
607         // append default value if instances could not be identified
608         if (instances.isEmpty() && field.hasDefaultValue()) {
609             instances.append(field.defaultValue().toString());
610         }
611
612         return instances;
613     }
614
615     // otherwise apply single value
616     bool hasInstanceId = false;
617     const int instanceId = fieldStringWithStrippedGraph(field, rawValueString, isReadOnly).toInt(&hasInstanceId);
618
619     if (hasInstanceId) {
620         foreach(const InstanceInfoBase &instance, field.allowableInstances()) {
621             if (trackerId(instance) == instanceId) {
622                 return instance.value();
623             }
624         }
625     }
626
627     qctWarn(QString::fromLatin1("Unknown instance id for field %1").
628             arg(field.name()));
629
630     // apply default value if instances could not be identified
631     if (field.hasDefaultValue()) {
632         return field.defaultValue();
633     } else {
634         return QVariant();
635     }
636 }
637
638 /// Returns @c true if @p variant is an empty string or an empty stringlist.
639 static bool
640 isEmptyStringOrStringList(const QVariant &variant)
641 {
642     return((QVariant::String == variant.type() && variant.toString().isEmpty()) ||
643            (QVariant::StringList == variant.type() && variant.toStringList().isEmpty()));
644 }
645
646 QVariant
647 QTrackerAbstractContactFetchRequest::fetchField(const QTrackerContactDetailField &field,
648                                                 const QString &rawValueString,
649                                                 bool &isReadOnly ) const
650 {
651     // set default assumption
652     isReadOnly = false;
653
654     if (field.hasSubTypeClasses()) {
655         return fetchSubTypesClasses(field, rawValueString);
656     }
657
658     if (rawValueString.isEmpty()) {
659         return QVariant();
660     }
661
662     if (field.hasSubTypeProperties()) {
663         return QVariant(fieldStringWithStrippedGraph(field, rawValueString, isReadOnly));
664     }
665
666     if (field.restrictsValues()) {
667         if (not field.allowableInstances().isEmpty()) {
668             return fetchInstances(field, rawValueString, isReadOnly);
669         }
670
671         if (not field.allowableValues().isEmpty()) {
672             if (QVariant::StringList == field.dataType()) {
673                 return fieldStringListWithStrippedGraph(field, rawValueString, isReadOnly);
674             } else {
675                 return fieldStringWithStrippedGraph(field, rawValueString, isReadOnly);
676             }
677         }
678     }
679
680     const QString valueString = fieldStringWithStrippedGraph(field, rawValueString, isReadOnly);
681
682     QVariant parsedValue;
683     if (not field.parseValue(valueString, parsedValue)) {
684         qctWarn(QString::fromLatin1("Cannot convert value to %1 for field %2: %3").
685                 arg(QLatin1String(QVariant::typeToName(field.dataType())),
686                     field.name(), valueString));
687
688         return QVariant();
689     }
690
691     if (isEmptyStringOrStringList(parsedValue)) {
692         return QVariant();
693     }
694
695     return parsedValue;
696 }
697
698 void
699 QTrackerAbstractContactFetchRequest::fetchCustomValues(const QTrackerContactDetailField &field,
700                                                        QVariant &fieldValue,
701                                                        const QString &rawValueString,
702                                                        bool &isReadOnly) const
703 {
704     switch (field.dataType()) {
705     case QVariant::StringList: {
706         QStringList values =
707                 fieldValue.toStringList() +
708                 fieldStringListWithStrippedGraph(field, rawValueString, isReadOnly, true);
709
710         if (not values.isEmpty()) {
711             fieldValue = values;
712         } else {
713             fieldValue = QVariant();
714         }
715
716         break;
717     }
718
719     case QVariant::String: {
720         if (not rawValueString.isEmpty()) {
721             fieldValue = fieldStringWithStrippedGraph(field, rawValueString, isReadOnly);
722         }
723
724         break;
725     }
726
727     default:
728         qctWarn(QString::fromLatin1("Cannot fetch custom values for field %2: "
729                                     "Data type %3 is not supported yet.").
730                 arg(field.name(), QLatin1String(QVariant::typeToName(field.dataType()))));
731         break;
732     }
733 }
734
735 bool
736 QTrackerAbstractContactFetchRequest::saveDetail(ContactCache::iterator contact,
737                                                 QContactDetail &detail,
738                                                 const QTrackerContactDetail &definition)
739 {
740     if (not detail.detailUri().isEmpty()) {
741         QString detailUri;
742
743         // Detail URI scheme is different from resource IRI scheme
744         // for details like QContactPresence or online avatars.
745         if (definition.detailUriScheme() != definition.resourceIriScheme()) {
746             bool valid = false;
747             QVariant value;
748
749             value = QTrackerContactSubject::parseIri(definition.resourceIriScheme(),
750                                                      detail.detailUri(), &valid);
751
752             if (valid) {
753                 detailUri = QTrackerContactSubject::makeIri(definition.detailUriScheme(),
754                                                             QVariantList() << value);
755             }
756         }
757
758         if (not detailUri.isEmpty()) {
759             detailUri = Utils::unescapeIri(detailUri);
760             detail.setLinkedDetailUris(detail.detailUri());
761             detail.setDetailUri(detailUri);
762         }
763     }
764
765     return contact->saveDetail(&detail);
766 }
767
768 void
769 QTrackerAbstractContactFetchRequest::fetchUniqueDetail(QList<QContactDetail> &details,
770                                                        const QueryContext &queryContext,
771                                                        const DetailContext &context,
772                                                        const QString &affiliation)
773 {
774     QContactDetail detail(context.definition().name());
775
776     int lastColumn = context.firstColumn();
777
778     // FIXME: we don't support subtypes by class for unique details
779
780     bool empty = true;
781     bool isDetailReadOnly = false;
782
783     foreach(const QTrackerContactDetailField &field, context.definition().fields()) {
784         if (not field.hasPropertyChain()) {
785             continue;
786         }
787
788         const QString rawValueString = queryContext.result->stringValue(lastColumn++);
789
790         if (rawValueString.isEmpty()) {
791             continue;
792         }
793
794         bool isFieldReadOnly = false;
795         const QVariant fieldValue = fetchField(field, rawValueString, isFieldReadOnly);
796
797         if (fieldValue.isNull()) {
798             continue;
799         }
800
801         // if any field is not owned by qct, set whole detail readonly (can't do that on field level)
802         if (isFieldReadOnly) {
803             isDetailReadOnly = true;
804         }
805
806         if (not field.hasSubTypes()) {
807             detail.setValue(field.name(), fieldValue);
808             empty = false;
809         } else {
810             if (field.hasSubTypeProperties()) {
811                 // collect subtypes by those property subtype columns which have some string set
812                 QSet<QString> subTypes;
813
814                 foreach(const PropertyInfoBase &pi, field.subTypeProperties()) {
815                     if (lastColumn == queryContext.query.projections().count()) {
816                         qctWarn(QString::fromLatin1("Trying to fetch more detail fields than we have "
817                                                     "columns for field %1 subtypes").
818                                 arg(field.name()));
819                         return;
820                     }
821
822                     if (not queryContext.result->stringValue(lastColumn++).isEmpty()) {
823                         subTypes.insert(pi.value().toString());
824                     }
825                 }
826
827                 detail.setValue(field.name(), fetchSubTypes(field, subTypes));
828             }
829         }
830     }
831
832     // if detail is empty, return as it is ignored - no need to update uri
833     if (empty) {
834         return;
835     }
836
837     // if any field is not owned by qct, set whole detail readonly (can't do that on field level)
838     if (isDetailReadOnly) {
839         QContactManagerEngine::setDetailAccessConstraints(&detail, detail.accessConstraints()|QContactDetail::ReadOnly);
840     }
841
842     // update uri
843     if (context.definition().hasDetailUri()) {
844         // If the detailUri was on nco:hasAffiliation, we don't have anything to
845         // do since we already know the IRI of the affiliation. If it was on
846         // another property, it is always stored in the first column
847         const QTrackerContactDetailField *detailUriField = context.definition().detailUriField();
848
849         foreach (const PropertyInfoBase &pi, detailUriField->propertyChain()) {
850             if (pi.hasDetailUri()) {
851                 if (pi.iri() == nco::hasAffiliation::iri()) {
852                     detail.setDetailUri(Utils::unescapeIri(affiliation));
853                 } else {
854                     detail.setDetailUri(Utils::unescapeIri(queryContext.result->stringValue(lastColumn++)));
855                 }
856                 break;
857             }
858         }
859     }
860
861     details.append(detail);
862 }
863
864 static void
865 fetchMultiDetailUri(QContactDetail &detail,
866                     const QTrackerContactDetail& definition,
867                     QStringList &fieldsData)
868 {
869     if (definition.hasDetailUri()) {
870         // If the detailUri was on nco:hasAffiliation, we don't have anything to
871         // do since we already know the IRI of the affiliation. If it was on
872         // another property, it is always stored in the first column
873         const QTrackerContactDetailField *detailUriField = definition.detailUriField();
874
875         PropertyInfoList::ConstIterator pi = detailUriField->propertyChain().constBegin();
876
877         for(; pi != detailUriField->propertyChain().constEnd(); ++pi) {
878             if (pi->hasDetailUri()) {
879                 detail.setDetailUri(Utils::unescapeIri(fieldsData.takeFirst()));
880                 break;
881             }
882         }
883     }
884 }
885
886 /// creates a copy of @p _fields with all fields with an inverted property in the chain at the end
887 static QList<QTrackerContactDetailField>
888 moveWithInversePropertyAtEnd(const QList<QTrackerContactDetailField>& fields)
889 {
890     QList<QTrackerContactDetailField> sortedFields;
891     QList<QTrackerContactDetailField> inverseFields;
892
893     foreach(const QTrackerContactDetailField &field, fields) {
894         if (not field.hasPropertyChain()) {
895             continue;
896         }
897
898         if (field.propertyChain().hasInverseProperty()) {
899             inverseFields.append(field);
900         } else {
901             sortedFields.append(field);
902         }
903     }
904
905     sortedFields += inverseFields;
906     return sortedFields;
907 }
908
909 void
910 QTrackerAbstractContactFetchRequest::fetchMultiDetail(QContactDetail &detail,
911                                                       const DetailContext &context,
912                                                       const QString &rawValueString)
913 {
914     QStringList fieldsData = rawValueString.split(QTrackerScalarContactQueryBuilder::fieldSeparator());
915
916     fetchMultiDetailUri(detail, context.definition(), fieldsData);
917
918     // Fields that have inverse properties are always stored last, so respect
919     // this order when fetching them
920     const QList<QTrackerContactDetailField> fields =
921         moveWithInversePropertyAtEnd(context.definition().fields());
922
923     bool isDetailReadOnly = false;
924     foreach(const QTrackerContactDetailField &field, fields) {
925         // Property subtypes are done in unifyPropertySubTypes
926         if (field.hasSubTypeProperties()) {
927             continue;
928         }
929
930         if (fieldsData.empty()) {
931             qctWarn(QString::fromLatin1("Trying to fetch more detail fields than we have "
932                                         "columns for detail %1").arg(context.definition().name()));
933             return;
934         }
935
936         QVariant fieldValue;
937         bool isFieldReadOnly = false;
938
939         if (not field.isWithoutMapping()) {
940             fieldValue = fetchField(field, fieldsData.takeFirst(), isFieldReadOnly);
941         }
942
943         if (field.permitsCustomValues()) {
944             if (fieldsData.empty()) {
945                 qctWarn(QString::fromLatin1("Missing custom values for field %1 of detail %2").
946                                             arg(field.name(), context.definition().name()));
947             }
948
949             fetchCustomValues(field, fieldValue, fieldsData.takeFirst(), isFieldReadOnly);
950         }
951
952         if (isFieldReadOnly) {
953             isDetailReadOnly = true;
954         }
955
956         if (not fieldValue.isNull()) {
957             detail.setValue(field.name(), fieldValue);
958         }
959     }
960     // if any field is not owned by qct, set whole detail readonly (can't do that on field level)
961     if (isDetailReadOnly) {
962         QContactManagerEngine::setDetailAccessConstraints(&detail, detail.accessConstraints()|QContactDetail::ReadOnly);
963     }
964 }
965
966 /* Enhances \sa QContactDetail::isEmpty by ignoring context and detail uri - stored also as values */
967 static bool
968 areContactDetailDataValuesEmpty(const QContactDetail &detail)
969 {
970     const QVariantMap variantValues = detail.variantValues();
971     if (variantValues.empty()) {
972         return true;
973     }
974     if (variantValues.size() <= 2) {
975         QStringList keys(variantValues.keys());
976         keys.removeOne(QContactDetail::FieldDetailUri);
977         keys.removeOne(QContactDetail::FieldLinkedDetailUris);
978         return (keys.size() == 0);
979     }
980     return false;
981 }
982
983 void
984 QTrackerAbstractContactFetchRequest::fetchMultiDetails(QList<QContactDetail> &details,
985                                                        const QueryContext &queryContext,
986                                                        const DetailContext &detailContext)
987 {
988     int lastColumn = detailContext.firstColumn();
989
990     const QTrackerContactDetailField *subTypeField = detailContext.definition().subTypeField();
991
992     // First demarshall the raw data
993     const QString rawValueString = queryContext.result->stringValue(lastColumn++);
994
995     if (rawValueString.isEmpty()) {
996         return;
997     }
998
999     QHash<QString, QString> subTypesDetailsData;
1000     subTypesDetailsData.insert(QString(), rawValueString);
1001
1002     // Subtype properties are spread over various details (one for
1003     // each subtype)
1004     // If we have subtype properties, fetch as many details as we
1005     // have subtypes
1006     if (subTypeField && subTypeField->hasSubTypeProperties()) {
1007         // Only used for debug message
1008         const QString &definitionName = detailContext.definition().name();
1009
1010         foreach(const PropertyInfoBase &pi, subTypeField->subTypeProperties()) {
1011             if (lastColumn > detailContext.lastColumn()) {
1012                 qctWarn(QString::fromLatin1("Trying to fetch more subtype details "
1013                                             "than we have columns for detail %1").
1014                         arg(definitionName));
1015                 return;
1016             }
1017
1018             const QString rawSubTypeData = queryContext.result->stringValue(lastColumn++);
1019
1020             if (rawSubTypeData.isEmpty()) {
1021                 continue;
1022             }
1023
1024             subTypesDetailsData.insert(pi.value().toString(), rawSubTypeData);
1025         }
1026     }
1027
1028     // Now parse each fetched detail into a proper QContactDetail,
1029     // keeping the info about its subtype
1030     QMultiHash<QString, QContactDetail> subTypeDetails;
1031     for (QHash<QString,QString>::ConstIterator it = subTypesDetailsData.constBegin();
1032          it != subTypesDetailsData.constEnd(); ++it ) {
1033         const QString &subType = it.key();
1034         const QString &rawDetailsData = it.value();
1035         const QStringList detailsData = rawDetailsData.split(QTrackerScalarContactQueryBuilder::detailSeparator());
1036
1037         foreach(const QString &detailData, detailsData) {
1038             if (detailData.isEmpty()) {
1039                 continue;
1040             }
1041
1042             QContactDetail detail(detailContext.definition().name());
1043
1044             fetchMultiDetail(detail, detailContext, detailData);
1045             subTypeDetails.insert(subType,detail);
1046         }
1047     }
1048
1049     // And finally unify the fetched details
1050
1051     // There is at least one key, the one of the default type
1052     if (subTypeField && subTypeField->hasSubTypeProperties()) {
1053         details.append(unifyPropertySubTypes(subTypeDetails, *subTypeField));
1054     } else {
1055         details.append(subTypeDetails.values());
1056     }
1057 }
1058
1059 void
1060 QTrackerAbstractContactFetchRequest::fetchResults(ContactCache &results, QueryContext &queryContext)
1061 {
1062     QSparqlResult *const result = queryContext.result;
1063
1064     for(bool hasRow = result->first(); not isCanceled() && hasRow; hasRow = result->next()) {
1065         // identify the contact
1066         const QString contactIri = result->stringValue(0);
1067         const QContactLocalId localId = result->value(1).toUInt();
1068         const QString affiliation = result->stringValue(2);
1069         const QString affiliationContext = qctCamelCase(result->stringValue(3));
1070
1071         // create contact if not already existing
1072         ContactCache::Iterator contact(results.find(localId));
1073
1074         if(contact == results.end()) {
1075             QContact c;
1076             QContactId id;
1077             id.setLocalId(localId);
1078             id.setManagerUri(engine()->managerUri());
1079             c.setId(id);
1080             c.setType(queryContext.contactType());
1081
1082             contact = results.insert(localId, c);
1083             queryContext.contactIds.append(localId);
1084         }
1085
1086         // read details
1087         for(QList<DetailContext>::ConstIterator detailContext = queryContext.details.constBegin();
1088             detailContext != queryContext.details.constEnd();
1089             ++detailContext) {
1090             QList<QContactDetail> details;
1091
1092             if (detailContext->definition().isUnique()) {
1093                 fetchUniqueDetail(details, queryContext, *detailContext, affiliation);
1094             } else {
1095                 fetchMultiDetails(details, queryContext, *detailContext);
1096             }
1097
1098             foreach(QContactDetail detail, details) {
1099                 // Ignore the detail if we fetched no fields for it (or just detailUri)
1100                 if (areContactDetailDataValuesEmpty(detail)) {
1101                     continue;
1102                 }
1103
1104                 // No context is added for empty context string.
1105                 if (detailContext->definition().hasContext() && not affiliationContext.isEmpty()) {
1106                     detail.setContexts(affiliationContext);
1107                 }
1108
1109                 if (not saveDetail(contact, detail, detailContext->definition())) {
1110                     qctWarn(QString::fromLatin1("Could not save detail %1 on contact %2").
1111                             arg (detail.definitionName(), contactIri));
1112                 }
1113             }
1114         }
1115
1116         fetchCustomDetails(queryContext, contact);
1117
1118         fetchHasMemberRelationships(queryContext, contact);
1119     }
1120 }
1121
1122 QContactDetail
1123 QTrackerAbstractContactFetchRequest::fetchCustomDetail(const QString &rawValue, const QString &contactType)
1124 {
1125     QStringList tokens = rawValue.split(QTrackerScalarContactQueryBuilder::fieldSeparator());
1126
1127     // Minimum number of tokens is detail name + 1 field name/value tuple
1128     if (tokens.size() < 3) {
1129         return QContactDetail();
1130     }
1131
1132     QMultiHash<QString, QString> detailValues;
1133     QString detailName = tokens.takeFirst();
1134
1135     QContactDetail detail(detailName);
1136
1137     const QContactDetailDefinitionMap detailDefs = engine()->detailDefinitions(contactType, 0);
1138
1139
1140     while(tokens.size() >= 2) {
1141         QString fieldName = tokens.takeFirst();
1142         QStringList fieldValues = tokens.takeFirst().split(QTrackerScalarContactQueryBuilder::listSeparator());
1143         foreach(const QString &f, fieldValues) {
1144             detailValues.insert(fieldName, f);
1145         }
1146     }
1147
1148     foreach(const QString &fieldName, detailValues.uniqueKeys()) {
1149         QMap<uint, QString> orderedValues;
1150
1151         // Values are retrieved as "tracker-id:value" pairs.
1152         // Order values by tracker-id and extract the value.
1153         foreach(const QString &s, detailValues.values(fieldName)) {
1154             const int i = s.indexOf(QLatin1Char(':'));
1155             orderedValues.insert(s.left(i).toUInt(), s.mid(i + 1));
1156         }
1157
1158         // QVariant cannot deal with QList<QString> :-/
1159         const QStringList fieldValues = orderedValues.values();
1160         QVariant fieldValue;
1161
1162         if (fieldValues.size() == 1) {
1163             fieldValue = fieldValues.first();
1164         } else {
1165             fieldValue = fieldValues;
1166         }
1167
1168         detail.setValue(fieldName, fieldValue);
1169
1170         const QContactDetailFieldDefinitionMap fieldDefs = detailDefs[detail.definitionName()].fields();
1171         const QContactDetailFieldDefinition fieldDef = fieldDefs.value(fieldName);
1172
1173         if (QVariant::Invalid != fieldDef.dataType() && fieldValue.convert(fieldDef.dataType())) {
1174             detail.setValue(fieldName, fieldValue);
1175         }
1176     }
1177
1178     return detail;
1179 }
1180
1181 void
1182 QTrackerAbstractContactFetchRequest::fetchCustomDetails(const QueryContext &queryContext,
1183                                                         ContactCache::Iterator contact)
1184 {
1185     if (queryContext.customDetailColumn < 0 ||
1186         queryContext.customDetailColumn >= queryContext.query.projections().count()) {
1187         return;
1188     }
1189
1190     const QString rawValue = queryContext.result->stringValue(queryContext.customDetailColumn);
1191
1192     foreach(const QString &rawDetailValue, rawValue.split(QTrackerScalarContactQueryBuilder::detailSeparator())) {
1193         QContactDetail detail = fetchCustomDetail(rawDetailValue, contact->type());
1194
1195         if (not areContactDetailDataValuesEmpty(detail)) {
1196             contact->saveDetail(&detail);
1197         }
1198     }
1199 }
1200
1201 void
1202 QTrackerAbstractContactFetchRequest::fetchHasMemberRelationships(const QueryContext &queryContext,
1203                                                                  ContactCache::Iterator contact)
1204 {
1205     if (queryContext.hasMemberRelationshipColumn < 0 ||
1206         queryContext.hasMemberRelationshipColumn >= queryContext.query.projections().count()) {
1207         return;
1208     }
1209
1210     QList<QContactRelationship> hasMemberRelationships;
1211
1212     QContactRelationship relationship;
1213     relationship.setRelationshipType(QContactRelationship::HasMember);
1214
1215     QContactId contactId;
1216     contactId.setManagerUri(engine()->managerUri());
1217     contactId.setLocalId(contact->localId());
1218
1219     QContactId otherContactId;
1220     otherContactId.setManagerUri(engine()->managerUri());
1221
1222     // HasMember with contact in Second role, valid for both group and normal contacts
1223     int currentColumn = queryContext.hasMemberRelationshipColumn;
1224     const QString rawValue = queryContext.result->stringValue(currentColumn++);
1225
1226     if (not rawValue.isEmpty()) {
1227         relationship.setSecond(contactId);
1228
1229         const QStringList localIdList = rawValue.split(QTrackerScalarContactQueryBuilder::listSeparator());
1230         foreach(const QString &localIdString, localIdList) {
1231             bool isValidId = false;
1232             const QContactLocalId localId = localIdString.toUInt(&isValidId);
1233
1234             if (isValidId) {
1235                 otherContactId.setLocalId(localId);
1236                 relationship.setFirst(otherContactId);
1237                 hasMemberRelationships << relationship;
1238             }
1239         }
1240     }
1241
1242     if (queryContext.contactType() == QContactType::TypeGroup) {
1243         // HasMember with contact in First role, only applyable for group contacts
1244         const QString rawValue = queryContext.result->stringValue(currentColumn++);
1245
1246         if (not rawValue.isEmpty()) {
1247             relationship.setFirst(contactId);
1248
1249             const QStringList localIdList = rawValue.split(QTrackerScalarContactQueryBuilder::listSeparator());
1250
1251             foreach(const QString &localIdString, localIdList) {
1252                 bool isValidId = false;
1253                 const QContactLocalId localId = localIdString.toUInt(&isValidId);
1254
1255                 if (isValidId) {
1256                     otherContactId.setLocalId(localId);
1257                     relationship.setSecond(otherContactId);
1258                     hasMemberRelationships << relationship;
1259                 }
1260             }
1261         }
1262     }
1263
1264     QContactManagerEngine::setContactRelationships(&contact.value(), hasMemberRelationships);
1265 }
1266
1267 static void
1268 updateDetailLinks(QContact &contact)
1269 {
1270     typedef QHash<QString, QContactDetail> DetailHash;
1271
1272     DetailHash detailHash;
1273
1274     foreach(const QContactDetail &detail, contact.details()) {
1275         if (not detail.detailUri().isEmpty()) {
1276             detailHash.insert(detail.detailUri(), detail);
1277         }
1278     }
1279
1280     for(DetailHash::ConstIterator detail = detailHash.constBegin(); detail != detailHash.constEnd(); ++detail) {
1281         foreach(const QString &detailUri, detail->linkedDetailUris()) {
1282             DetailHash::Iterator target = detailHash.find(detailUri);
1283
1284             if (target == detailHash.constEnd()) {
1285                 continue;
1286             }
1287
1288             QStringList linkedDetailUris = target->linkedDetailUris();
1289
1290             if (linkedDetailUris.contains(detail->detailUri())) {
1291                 continue;
1292             }
1293
1294             linkedDetailUris.append(detail->detailUri());
1295             target->setLinkedDetailUris(linkedDetailUris);
1296             contact.saveDetail(&target.value());
1297         }
1298     }
1299 }
1300
1301 static void
1302 removeDuplicateDetails(QContact &contact)
1303 {
1304     QSet<QContactDetail> knownDetails;
1305
1306     foreach(QContactDetail detail, contact.details()) {
1307         if(knownDetails.contains(detail)) {
1308             contact.removeDetail(&detail);
1309         } else {
1310             knownDetails.insert(detail);
1311         }
1312     }
1313 }
1314
1315 void
1316 QTrackerAbstractContactFetchRequest::run()
1317 {
1318     if (isCanceled()) {
1319         return;
1320     }
1321
1322     const QStringList &detailHint = m_fetchHint.detailDefinitionsHint();
1323     const bool calculateGlobalPresence(detailHint.contains(QContactGlobalPresence::DefinitionName) || detailHint.isEmpty());
1324     const bool calculateDisplayLabel(detailHint.contains(QContactDisplayLabel::DefinitionName) || detailHint.isEmpty());
1325     const bool calculateAvatar(detailHint.contains(QContactAvatar::DefinitionName) || detailHint.isEmpty());
1326
1327     ContactCache results;
1328
1329     foreach(const QTrackerContactDetailSchema &schema, engine()->schemas())  {
1330         // build RDF query
1331         QueryContext context(schema);
1332
1333         const QContactManager::Error error = buildQuery(context);
1334
1335         if (QContactManager::NoError != error) {
1336             setLastError(error);
1337             return;
1338         }
1339
1340         // run the query
1341         const QSparqlQuery query(context.query.sparql());
1342         QScopedPointer<QSparqlResult> result(runQuery(query, SyncQueryOptions));
1343
1344         if (result.isNull()) {
1345             return; // runQuery() called reportError()
1346         }
1347
1348         context.result = result.data();
1349         fetchResults(results, context);
1350
1351         // Update synthetic details and detail links
1352         // That needs to be done before sorting
1353         foreach (QContactLocalId id, context.contactIds) {
1354             QContact &c = results[id];
1355
1356             removeDuplicateDetails(c);
1357
1358             if (calculateGlobalPresence) {
1359                 qctUpdateGlobalPresence(c);
1360             }
1361             if (calculateDisplayLabel) {
1362                 engine()->updateDisplayLabel(c, m_nameOrder);
1363             }
1364             if (calculateAvatar) {
1365                 engine()->updateAvatar(c);
1366             }
1367
1368             updateDetailLinks(c);
1369         }
1370
1371         if (context.sorted || m_sorting.isEmpty()) {
1372             m_sortedIds[context.contactType()] = context.contactIds;
1373         } else {
1374             qctWarn(QString::fromLatin1("Could not sort results for contact type %1, reverting "
1375                                         "to in memory sorting.").arg(schema.contactType()));
1376             const QList<QContact> contacts = getContacts(results, context.contactIds);
1377             m_sortedIds[context.contactType()] = QContactManagerEngine::sortContacts(contacts, m_sorting);
1378         }
1379     }
1380
1381     processResults(results);
1382 }
1383
1384 QList<QContact>
1385 QTrackerAbstractContactFetchRequest::getContacts(const ContactCache &cache,
1386                                                  const QList<QContactLocalId> &ids)
1387 {
1388     QList<QContact> contacts;
1389     contacts.reserve(ids.size());
1390
1391     foreach (QContactLocalId id, ids) {
1392         contacts.append(cache.value(id));
1393     }
1394
1395     return contacts;
1396 }
1397
1398 ///////////////////////////////////////////////////////////////////////////////////////////////////
1399
1400 #include "moc_abstractcontactfetchrequest.cpp"