<commit_before>/***************************************************************************
**
** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (<EMAIL>)
**
** This file is part of contactsd.
**
** If you have questions regarding the use of this file, please contact
** Nokia at <EMAIL>.
**
** This library is free software; you can redistribute it and/or
** modify it under the terms of the GNU Lesser General Public
** License version 2.1 as published by the Free Software Foundation
** and appearing in the file LICENSE.LGPL included in the packaging
** of this file.
**
****************************************************************************/

#include <TelepathyQt4/AvatarData>
#include <TelepathyQt4/ContactCapabilities>
#include <TelepathyQt4/ConnectionCapabilities>
#include <qtcontacts-tracker/phoneutils.h>

#include "cdtpstorage.h"
#include "debug.h"

using namespace Contactsd;

static const QString privateGraph = QString::fromLatin1("<urn:uuid:679293d4-60f0-49c7-8d63-f1528fe31f66>");
static const QString defaultGenerator = QString::fromLatin1("\"telepathy\"");
static const QString filterInSeparator = QString::fromLatin1(",");
static const QString defaultContactMe = QString::fromLatin1("nco:default-contact-me");
static const QString imAddressVar = QString::fromLatin1("?imAddress");
static const QString imContactVar = QString::fromLatin1("?imContact");
static const QString imAccountVar = QString::fromLatin1("?imAccount");

CDTpStorage::CDTpStorage(QObject *parent) : QObject(parent), mUpdateRunning(false)
{
    mUpdateTimer.setInterval(0);
    mUpdateTimer.setSingleShot(true);
    connect(&mUpdateTimer, SIGNAL(timeout()), SLOT(onUpdateQueueTimeout()));
}

CDTpStorage::~CDTpStorage()
{
}

void CDTpStorage::syncAccounts(const QList<CDTpAccountPtr> &accounts)
{
    CDTpQueryBuilder builder("SyncAccounts");

    /* Ensure the default-contact-me exists. NB#215973 */
    builder.createResource(defaultContactMe, "nco:PersonContact");

    QStringList imAccounts;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        imAccounts << literalIMAccount(accountWrapper);
    }

    /* Selection for all IMAccounts that does not exist anymore, and their IMAddresses */
    static const QString tmpl = QString::fromLatin1("?imAccount nco:hasIMContact ?imAddress.");
    QString selection = tmpl;
    if (!accounts.isEmpty()) {
        static const QString tmpl = QString::fromLatin1("\nFILTER (?imAccount NOT IN (%1)).");
        selection += tmpl.arg(imAccounts.join(filterInSeparator));
    }

    /* Purge contacts for accounts that does not exist anymore */
    CDTpQueryBuilder subBuilder("SyncAccounts - purge contacts");
    subBuilder.appendRawSelection(selection);
    addRemoveContactToBuilder(subBuilder, imAddressVar);
    builder.appendRawQuery(subBuilder);

    /* Purge accounts/imAddresses that does not exist anymore */
    subBuilder = CDTpQueryBuilder("SyncAccounts - purge imAddresses");
    subBuilder.appendRawSelection(selection);
    subBuilder.deleteResource(imAddressVar);
    builder.appendRawQuery(subBuilder);

    subBuilder = CDTpQueryBuilder("SyncAccounts - purge imAccount");
    subBuilder.appendRawSelection(selection);
    subBuilder.deleteResource(imAccountVar);
    builder.appendRawQuery(subBuilder);

    /* Sync accounts and their contacts */
    if (!accounts.isEmpty()) {
        subBuilder = CDTpQueryBuilder("SyncAccounts - sync accounts");
        addSyncAccountsToBuilder(subBuilder, accounts);
        builder.appendRawQuery(subBuilder);
    }

    /* Notify import progress for accounts that have contacts */
    QList<CDTpAccountPtr> rosterAccounts;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        if (!accountWrapper->contacts().isEmpty()) {
            rosterAccounts << accountWrapper;
            Q_EMIT syncStarted(accountWrapper);
        }
    }
    CDTpAccountsSparqlQuery *query = new CDTpAccountsSparqlQuery(rosterAccounts, builder, this);
    connect(query,
            SIGNAL(finished(CDTpSparqlQuery *)),
            SLOT(onSyncOperationEnded(CDTpSparqlQuery *)));
}

void CDTpStorage::syncAccount(CDTpAccountPtr accountWrapper)
{
    CDTpQueryBuilder builder("SyncAccount");

    /* Create account */
    QList<CDTpAccountPtr> accounts = QList<CDTpAccountPtr>() << accountWrapper;
    builder.updateProperty(defaultContactMe, "nie:contentLastModified", literalTimeStamp());
    addCreateAccountsToBuilder(builder, accounts);

    /* if account has no contacts, we are done */
    if (accountWrapper->contacts().isEmpty()) {
        new CDTpSparqlQuery(builder, this);
        return;
    }

    /* Create account's contacts */
    addCreateContactsToBuilder(builder, accountWrapper->contacts());

    /* Notify import progress */
    Q_EMIT syncStarted(accountWrapper);
    CDTpAccountsSparqlQuery *query = new CDTpAccountsSparqlQuery(accountWrapper, builder, this);
    connect(query,
            SIGNAL(finished(CDTpSparqlQuery *)),
            SLOT(onSyncOperationEnded(CDTpSparqlQuery *)));
}

void CDTpStorage::updateAccount(CDTpAccountPtr accountWrapper,
        CDTpAccount::Changes changes)
{
    debug() << "Update account" << literalIMAddress(accountWrapper);

    CDTpQueryBuilder builder("UpdateAccount");

    builder.updateProperty(defaultContactMe, "nie:contentLastModified", literalTimeStamp());

    addRemoveAccountsChangesToBuilder(builder,
        literalIMAddress(accountWrapper), literalIMAccount(accountWrapper),
        changes);
    addAccountChangesToBuilder(builder, accountWrapper, changes);

    new CDTpSparqlQuery(builder, this);
}

void CDTpStorage::removeAccount(CDTpAccountPtr accountWrapper)
{
    const QString imAccount = literalIMAccount(accountWrapper);
    debug() << "Remove account" << imAccount;

    /* Bind imAddress to all contacts of this imAccount */
    static const QString tmpl = QString::fromLatin1("%1 nco:hasIMContact ?imAddress.");
    const QString selection = tmpl.arg(imAccount);

    /* Remove account's contacts */
    CDTpQueryBuilder builder("RemoveAccount - contacts");
    builder.appendRawSelection(selection);
    addRemoveContactToBuilder(builder, imAddressVar);

    /* Remove imAddress */
    CDTpQueryBuilder subBuilder("RemoveAccount - imAddress");
    subBuilder.appendRawSelection(selection);
    subBuilder.deleteResource(imAddressVar);
    builder.appendRawQuery(subBuilder);

    /* Remove account */
    subBuilder = CDTpQueryBuilder("RemoveAccount - account");
    subBuilder.deleteResource(imAccount);
    builder.appendRawQuery(subBuilder);

    new CDTpSparqlQuery(builder, this);
}

void CDTpStorage::addSyncAccountsToBuilder(CDTpQueryBuilder &builder,
        const QList<CDTpAccountPtr> &accounts) const
{
    QStringList imAccounts;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        imAccounts << literalIMAccount(accountWrapper);
    }

    // Drop all mutable properties in all accounts at once, then (re-)create them
    static const QString tmpl = QString::fromLatin1(
            "?imAccount nco:imAccountAddress ?imAddress.\n"
            "FILTER(?imAccount IN (%1)).");
    builder.appendRawSelection(tmpl.arg(imAccounts.join(filterInSeparator)));

    addRemoveAccountsChangesToBuilder(builder, imAddressVar, imAccountVar, CDTpAccount::All);

    CDTpQueryBuilder subBuilder("CreateAccounts");

    subBuilder.updateProperty(defaultContactMe, "nie:contentLastModified", literalTimeStamp());
    addCreateAccountsToBuilder(subBuilder, accounts);
    builder.appendRawQuery(subBuilder);

    // Sync all contacts of all accounts. Special case for no-roster/offline
    // or disabled accounts.
    QList<CDTpAccountPtr> rosterAccounts;
    QList<CDTpAccountPtr> disabledAccounts;
    QList<CDTpAccountPtr> noRosterAccounts;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        if (accountWrapper->hasRoster()) {
            rosterAccounts << accountWrapper;
        } else if (!accountWrapper->account()->isEnabled()) {
            disabledAccounts << accountWrapper;
        } else {
            noRosterAccounts << accountWrapper;
        }
    }

    if (!rosterAccounts.isEmpty()) {
        subBuilder = CDTpQueryBuilder("SyncRosterAccounts");
        addSyncRosterAccountsContactsToBuilder(subBuilder, rosterAccounts);
        builder.appendRawQuery(subBuilder);
    }

    if (!disabledAccounts.isEmpty()) {
        subBuilder = CDTpQueryBuilder("SyncDisabledAccounts");
        addSyncDisabledAccountsContactsToBuilder(subBuilder, disabledAccounts);
        builder.appendRawQuery(subBuilder);
    }

    if (!noRosterAccounts.isEmpty()) {
        subBuilder = CDTpQueryBuilder("SyncNoRosterAccounts");
        addSyncNoRosterAccountsContactsToBuilder(subBuilder, noRosterAccounts);
        builder.appendRawQuery(subBuilder);
    }
}

void CDTpStorage::addCreateAccountsToBuilder(CDTpQueryBuilder &builder,
        const QList<CDTpAccountPtr> &accounts) const
{
    QStringList imAddresses;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        Tp::AccountPtr account = accountWrapper->account();
        const QString imAccount = literalIMAccount(accountWrapper);
        const QString imAddress = literalIMAddress(accountWrapper);

        debug() << "Create account" << imAddress;

        // Ensure the IMAccount exists
        builder.createResource(imAccount, "nco:IMAccount", privateGraph);
        builder.insertProperty(imAccount, "nco:imAccountType", literal(account->protocolName()), privateGraph);
        builder.insertProperty(imAccount, "nco:imAccountAddress", imAddress, privateGraph);
        builder.insertProperty(imAccount, "nco:hasIMContact", imAddress, privateGraph);

        // Ensure the self contact has an IMAddress
        builder.createResource(imAddress, "nco:IMAddress", privateGraph);
        builder.insertProperty(imAddress, "nco:imID", literal(account->normalizedName()), privateGraph);
        builder.insertProperty(imAddress, "nco:imProtocol", literal(account->protocolName()), privateGraph);

        // Add all mutable properties
        addAccountChangesToBuilder(builder, accountWrapper, CDTpAccount::All);

        imAddresses << imAddress;
    }

    // Ensure the IMAddresses are bound to the default-contact-me via an affiliation
    static const QString tmpl = QString::fromLatin1(
        "INSERT {\n"
        "    GRAPH %1 {\n"
        "        nco:default-contact-me nco:hasAffiliation _:affiliation.\n"
        "        _:affiliation a nco:Affiliation;\n"
        "                      nco:hasIMAddress ?imAddress;\n"
        "                      rdfs:label \"Other\".\n"
        "    }\n"
        "}\n"
        "WHERE {\n"
        "    ?imAddress a nco:IMAddress.\n"
        "    FILTER (?imAddress IN (%2) &&\n"
        "            NOT EXISTS { nco:default-contact-me nco:hasAffiliation [ nco:hasIMAddress ?imAddress ] })\n"
        "}\n");
    builder.appendRawQuery(tmpl.arg(privateGraph).arg(imAddresses.join(filterInSeparator)));
}

void CDTpStorage::addRemoveAccountsChangesToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        const QString &imAccount,
        CDTpAccount::Changes changes) const
{
    if (changes & CDTpAccount::Presence) {
        addRemovePresenceToBuilder(builder, imAddress);
    }
    if (changes & CDTpAccount::Avatar) {
        addRemoveAvatarToBuilder(builder, imAddress);
    }
    if (changes & CDTpAccount::Nickname) {
        builder.deleteProperty(imAddress, "nco:imNickname");
    }
    if (changes & CDTpAccount::DisplayName) {
        builder.deleteProperty(imAccount, "nco:imDisplayName");
    }
}

void CDTpStorage::addAccountChangesToBuilder(CDTpQueryBuilder &builder,
        CDTpAccountPtr accountWrapper,
        CDTpAccount::Changes changes) const
{
    Tp::AccountPtr account = accountWrapper->account();
    const QString imAccount = literalIMAccount(accountWrapper);
    const QString imAddress = literalIMAddress(accountWrapper);

    if (changes & CDTpAccount::Presence) {
        debug() << "  presence changed";
        addPresenceToBuilder(builder, imAddress, account->currentPresence());
    }
    if (changes & CDTpAccount::Avatar) {
        debug() << "  avatar changed";
        addAvatarToBuilder(builder, imAddress, saveAccountAvatar(accountWrapper));
    }
    if (changes & CDTpAccount::Nickname) {
        debug() << "  nickname changed";
        builder.insertProperty(imAddress, "nco:imNickname", literal(account->nickname()),
                privateGraph);
    }
    if (changes & CDTpAccount::DisplayName) {
        debug() << "  display name changed";
        builder.insertProperty(imAccount, "nco:imDisplayName", literal(account->displayName()),
                privateGraph);
    }
}

QString CDTpStorage::saveAccountAvatar(CDTpAccountPtr accountWrapper) const
{
    const Tp::Avatar &avatar = accountWrapper->account()->avatar();

    if (avatar.avatarData.isEmpty()) {
        return QString();
    }

    static const QString tmpl = QString::fromLatin1("%1/.contacts/avatars/%2");
    QString fileName = tmpl.arg(QDir::homePath())
        .arg(QLatin1String(QCryptographicHash::hash(avatar.avatarData, QCryptographicHash::Sha1).toHex()));
    debug() << "Saving account avatar to" << fileName;

    QFile avatarFile(fileName);
    if (!avatarFile.open(QIODevice::WriteOnly)) {
        warning() << "Unable to save account avatar: error opening avatar "
            "file" << fileName << "for writing";
        return QString();
    }
    avatarFile.write(avatar.avatarData);
    avatarFile.close();

    return fileName;
}

void CDTpStorage::syncAccountContacts(CDTpAccountPtr accountWrapper)
{
    CDTpQueryBuilder builder("SyncAccountContacts");

    QList<CDTpAccountPtr> accounts = QList<CDTpAccountPtr>() << accountWrapper;
    if (accountWrapper->hasRoster()) {
        addSyncRosterAccountsContactsToBuilder(builder, accounts);
    } else if (!accountWrapper->account()->isEnabled()) {
        addSyncDisabledAccountsContactsToBuilder(builder, accounts);
    } else {
        addSyncNoRosterAccountsContactsToBuilder(builder, accounts);
    }

    /* if account has no contacts, we are done */
    if (accountWrapper->contacts().isEmpty()) {
        new CDTpSparqlQuery(builder, this);
        return;
    }

    /* Notify import progress */
    Q_EMIT syncStarted(accountWrapper);
    CDTpAccountsSparqlQuery *query = new CDTpAccountsSparqlQuery(accountWrapper, builder, this);
    connect(query,
            SIGNAL(finished(CDTpSparqlQuery *)),
            SLOT(onSyncOperationEnded(CDTpSparqlQuery *)));
}

void CDTpStorage::syncAccountContacts(CDTpAccountPtr accountWrapper,
        const QList<CDTpContactPtr> &contactsAdded,
        const QList<CDTpContactPtr> &contactsRemoved)
{
    CDTpQueryBuilder builder("Contacts Added/Removed");

    if (!contactsAdded.isEmpty()) {
        // delete the timestamp from PersonContact linked to imAddresses
        // (unlikely to exist). addCreateContactsToBuilder() will insert a new
        // value.
        QStringList imAddresses;
        Q_FOREACH (const CDTpContactPtr &contactWrapper, contactsAdded) {
            imAddresses << literalIMAddress(contactWrapper);
        }
        static const QString tmpl = QString::fromLatin1(
                "?imContact nco:hasAffiliation [ nco:hasIMAddress ?imAddress ].\n"
                "FILTER(?imAddress IN (%1)).");
        builder.appendRawSelection(tmpl.arg(imAddresses.join(filterInSeparator)));
        builder.deleteProperty(imContactVar, "nie:contentLastModified");

        // Create contacts
        CDTpQueryBuilder subBuilder("Contacts Added/Removed - Create contacts");
        addCreateContactsToBuilder(subBuilder, contactsAdded);
        builder.appendRawQuery(subBuilder);
    }

    if (!contactsRemoved.isEmpty()) {
        addRemoveContactsToBuilder(builder, accountWrapper, contactsRemoved);
    }

    new CDTpSparqlQuery(builder, this);
}

/* Use this only in offline mode - use syncAccountContacts in online mode */
void CDTpStorage::removeAccountContacts(const QString &accountPath, const QStringList &contactIds)
{
    CDTpQueryBuilder builder;
    addRemoveContactsToBuilder(builder, accountPath, contactIds);
    new CDTpSparqlQuery(builder, this);
}

void  CDTpStorage::contactsToAvoid(const QString &accountPath, const QStringList &contactIds)
{
   mContactsToAvoid[accountPath] = contactIds;
}

void CDTpStorage::updateContact(CDTpContactPtr contactWrapper, CDTpContact::Changes changes)
{
    if (!mUpdateQueue.contains(contactWrapper)) {
        mUpdateQueue.insert(contactWrapper, changes);
    } else {
        mUpdateQueue[contactWrapper] |= changes;
    }

    if (!mUpdateRunning) {
        mUpdateTimer.start();
    }
}

void CDTpStorage::onUpdateQueueTimeout()
{
    debug() << "Update" << mUpdateQueue.count() << "contacts";

    bool found = false;
    CDTpQueryBuilder builder("UpdateContacts");
    QHash<CDTpContactPtr, CDTpContact::Changes>::const_iterator i;
    for (i = mUpdateQueue.constBegin(); i != mUpdateQueue.constEnd(); i++) {
        CDTpContactPtr contactWrapper = i.key();
        CDTpContact::Changes changes = i.value();
        if (!contactWrapper->isVisible()) {
            continue;
        }

        CDTpQueryBuilder subBuilder("UpdateContact");

        // bind imContact to imAddresses
        static const QString tmpl = QString::fromLatin1(
                "?imContact nco:hasAffiliation [ nco:hasIMAddress %1 ].");

        const QString imAddress = literalIMAddress(contactWrapper);
        subBuilder.appendRawSelection(tmpl.arg(imAddress));
        addRemoveContactsChangesToBuilder(subBuilder, imAddress, imContactVar, changes);
        addContactChangesToBuilder(subBuilder, imAddress, imContactVar, changes, contactWrapper->contact());
        builder.appendRawQuery(subBuilder);

        found = true;
    }
    mUpdateQueue.clear();

    if (!found) {
        debug() << "  None needs update";
        return;
    }

    mUpdateRunning = true;
    CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
    connect(query,
            SIGNAL(finished(CDTpSparqlQuery *)),
            SLOT(onUpdateFinished(CDTpSparqlQuery *)));
}

void CDTpStorage::onUpdateFinished(CDTpSparqlQuery *query)
{
    Q_UNUSED(query);

    mUpdateRunning = false;
    if (!mUpdateQueue.isEmpty()) {
        mUpdateTimer.start();
    }
}

void CDTpStorage::addSyncNoRosterAccountsContactsToBuilder(CDTpQueryBuilder &builder,
        const QList<CDTpAccountPtr> accounts) const
{
    QStringList imAccounts;
    QStringList imAddresses;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        const QString imAddress = literalIMAddress(accountWrapper);
        const QString imAccount = literalIMAccount(accountWrapper);

        debug() << "Sync no roster account" << imAddress;

        imAddresses << imAddress;
        imAccounts << imAccount;
    }

    /* Bind ?imAccount to all no roster accounts.
     * Bind ?imAddress to all accounts' im contacts, except for the self contact
     * because we know it has presence OFFLINE, and that's done when updating
     * account.
     * Bind ?imContact to all contacts' local contact.
     */
    static const QString tmpl = QString::fromLatin1(
            "?imAccount nco:hasIMContact ?imAddress.\n"
            "?imContact nco:hasAffiliation [ nco:hasIMAddress ?imAddress ].\n"
            "FILTER(?imAddress NOT IN (%1) && ?imAccount IN (%2)).");
    builder.appendRawSelection(tmpl.arg(imAddresses.join(filterInSeparator)).arg(imAccounts.join(filterInSeparator)));

    // FIXME: we could use addPresenceToBuilder() somehow
    builder.updateProperty(imAddressVar, "nco:imPresence", presenceType(Tp::ConnectionPresenceTypeUnknown),
            privateGraph);
    builder.updateProperty(imAddressVar, "nco:presenceLastModified", literalTimeStamp(),
            privateGraph);
    builder.updateProperty(imContactVar, "nie:contentLastModified", literalTimeStamp());

    // Update capabilities of all contacts, since we don't know them anymore,
    // reset them to the account's caps.
    addRemoveCapabilitiesToBuilder(builder, imAddressVar);

    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        addCapabilitiesToBuilder(builder, literalIMAddress(accountWrapper),
                accountWrapper->account()->capabilities());
    }
}

void CDTpStorage::addSyncDisabledAccountsContactsToBuilder(CDTpQueryBuilder &builder,
        const QList<CDTpAccountPtr> accounts) const
{
    // FIXME: disabled for now
    addSyncNoRosterAccountsContactsToBuilder(builder, accounts);
}

void CDTpStorage::addSyncRosterAccountsContactsToBuilder(CDTpQueryBuilder &builder,
        const QList<CDTpAccountPtr> &accounts) const
{
    /* Remove all mutable info from all contacts of those accounts */
    addRemoveContactsChangesToBuilder(builder, accounts);

    /* Delete the hasIMContact property on IMAccounts (except for self contact)
     * then sync all contacts (that will add back hasIMContact for them). After
     * that we can purge all imAddresses not bound to an IMAccount anymore. */
    QStringList imAccounts;
    QStringList imAddresses;
    QList<CDTpContactPtr> allContacts;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        const QString imAddress = literalIMAddress(accountWrapper);
        const QString imAccount = literalIMAccount(accountWrapper);
        const QString accountPath = accountWrapper->account()->objectPath();

        debug() << "Sync roster account" << imAddress;

        imAccounts << imAccount;
        imAddresses << imAddress;
        Q_FOREACH(CDTpContactPtr contactPtr, accountWrapper->contacts()) {
            QStringList offlineRemoved = mContactsToAvoid[accountPath];
            if (!offlineRemoved.contains(contactPtr->contact()->id())) {
                allContacts << contactPtr;
            }
        }
    }
    CDTpQueryBuilder subBuilder = CDTpQueryBuilder("SyncRosterAccounts - drop hasIMContact");
    static const QString tmpl = QString::fromLatin1(
            "?imAccount nco:hasIMContact ?imAddress.\n"
            "FILTER(?imAddress NOT IN (%1) && ?imAccount IN (%2)).");
    subBuilder.appendRawSelection(tmpl.arg(imAddresses.join(filterInSeparator)).arg(imAccounts.join(filterInSeparator)));
    subBuilder.deleteProperty(imAccountVar, "nco:hasIMContact", imAddressVar);
    builder.appendRawQuery(subBuilder);

    /* Now create all contacts */
    if (!allContacts.isEmpty()) {
        subBuilder = CDTpQueryBuilder("CreateContacts");
        addCreateContactsToBuilder(subBuilder, allContacts);
        builder.appendRawQuery(subBuilder);
    }

    /* Bind imAddress to all contacts that does not exist anymore, to purge them */
    static const QString selection = QString::fromLatin1(
            "GRAPH %1 { ?imAddress a nco:IMAddress }.\n"
            "FILTER(NOT EXISTS { ?imAccount nco:hasIMContact ?imAddress }).")
            .arg(privateGraph);

    subBuilder = CDTpQueryBuilder("SyncRosterAccounts - purge contacts");
    subBuilder.appendRawSelection(selection);
    addRemoveContactToBuilder(subBuilder, imAddressVar);
    builder.appendRawQuery(subBuilder);

    subBuilder = CDTpQueryBuilder("SyncRosterAccounts - purge imAddresses");
    subBuilder.appendRawSelection(selection);
    subBuilder.deleteResource(imAddressVar);
    builder.appendRawQuery(subBuilder);
}

void CDTpStorage::addCreateContactsToBuilder(CDTpQueryBuilder &builder,
        const QList<CDTpContactPtr> &contacts) const
{
    // Ensure all imAddress exists and are linked from imAccount
    QStringList imAddresses;
    Q_FOREACH (const CDTpContactPtr contactWrapper, contacts) {
        CDTpAccountPtr accountWrapper = contactWrapper->accountWrapper();

        const QString imAddress = literalIMAddress(contactWrapper);
        builder.createResource(imAddress, "nco:IMAddress", privateGraph);
        builder.insertProperty(imAddress, "nco:imID", literal(contactWrapper->contact()->id()),
                privateGraph);
        builder.insertProperty(imAddress, "nco:imProtocol", literal(accountWrapper->account()->protocolName()),
                privateGraph);

        const QString imAccount = literalIMAccount(accountWrapper);
        builder.insertProperty(imAccount, "nco:hasIMContact", imAddress, privateGraph);

        // Add mutable properties except for ContactInfo
        addContactChangesToBuilder(builder, imAddress, CDTpContact::All,
                contactWrapper->contact());

        imAddresses << imAddress;
    }

    // Ensure all imAddresses are bound to a PersonContact
    static const QString tmpl = QString::fromLatin1(
        "INSERT {\n"
        "    GRAPH %1 {\n"
        "        _:contact a nco:PersonContact;\n"
        "                  nie:contentCreated %3;\n"
        "                  nie:generator %4.\n"
        "    }\n"
        "    GRAPH %2 {\n"
        "        _:contact nco:hasAffiliation _:affiliation.\n"
        "        _:affiliation a nco:Affiliation;\n"
        "                      nco:hasIMAddress ?imAddress;\n"
        "                      rdfs:label \"Other\".\n"
        "    }\n"
        "}\n"
        "WHERE {\n"
        "    GRAPH %2 { ?imAddress a nco:IMAddress }.\n"
        "    FILTER (NOT EXISTS { ?imContact nco:hasAffiliation [ nco:hasIMAddress ?imAddress ] })\n"
        "}\n");
    builder.appendRawQuery(tmpl.arg(CDTpQueryBuilder::defaultGraph).arg(privateGraph)
            .arg(literalTimeStamp()).arg(defaultGenerator));

    // Insert timestamp on all imContact bound to those imAddresses
    CDTpQueryBuilder subBuilder("CreateContacts - timestamp");
    static const QString tmpl2 = QString::fromLatin1(
            "?imContact nco:hasAffiliation [ nco:hasIMAddress ?imAddress ].\n"
            "FILTER(?imAddress IN (%1)).");
    subBuilder.appendRawSelection(tmpl2.arg(imAddresses.join(filterInSeparator)));
    subBuilder.insertProperty(imContactVar, "nie:contentLastModified", literalTimeStamp());
    builder.appendRawQuery(subBuilder);

    // Add ContactInfo seperately because we need to bind to the PersonContact
    // and that makes things much slower
    subBuilder = CDTpQueryBuilder("CreateContacts - add ContactInfo");
    uint imContactCount = 0;
    Q_FOREACH (const CDTpContactPtr contactWrapper, contacts) {
        if (!contactWrapper->isInformationKnown()) {
            continue;
        }

        const QString imAddress = literalIMAddress(contactWrapper);

        // Tracker supports at most 32 selection of the ?imContact_X
        if (imContactCount == 32) {
            builder.appendRawQuery(subBuilder);
            subBuilder = CDTpQueryBuilder("CreateContacts - add ContactInfo - next part");
            imContactCount = 0;
        }

        imContactCount++;
        const QString imContact = subBuilder.uniquify("?imContact");
        static const QString tmpl = QString::fromLatin1("%1 nco:hasAffiliation [ nco:hasIMAddress %2 ].");
        subBuilder.appendRawSelection(tmpl.arg(imContact).arg(imAddress));
        addContactInfoToBuilder(subBuilder, imAddress, imContact,
                contactWrapper->contact());
    }
    builder.appendRawQuery(subBuilder);
}

void CDTpStorage::addRemoveContactsChangesToBuilder(CDTpQueryBuilder &builder,
    const QList<CDTpAccountPtr> &accounts) const
{
    QStringList imAccounts;
    QStringList imAddresses;
    QList<CDTpContactPtr> allContacts;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        imAccounts << literalIMAccount(accountWrapper);
        imAddresses << literalIMAddress(accountWrapper);
        allContacts << accountWrapper->contacts();
    }

    if (allContacts.isEmpty()) {
        return;
    }

    /* We can easily drop all mutable properties at once for all contacts of
     * all those accounts. BUT Avatar and ContactInfo are special because they
     * could not be known yet in which case we want to keep the current value.
     * so we delete Avatar/Information later only for the contacts that already
     * have it. */

    uint mandatoryFlags = CDTpContact::All;
    mandatoryFlags &= ~CDTpContact::Information;
    mandatoryFlags &= ~CDTpContact::Avatar;
    static const QString tmpl = QString::fromLatin1(
            "?imAccount nco:hasIMContact ?imAddress.\n"
            "?imContact nco:hasAffiliation [ nco:hasIMAddress ?imAddress ].\n"
            "FILTER(?imAddress NOT IN (%1) && ?imAccount IN (%2)).");
    builder.appendRawSelection(tmpl.arg(imAddresses.join(filterInSeparator)).arg(imAccounts.join(filterInSeparator)));
    addRemoveContactsChangesToBuilder(builder, imAddressVar, imContactVar, CDTpContact::Changes(mandatoryFlags));

    QStringList avatarIMAddresses;
    QStringList infoIMAddresses;
    Q_FOREACH (const CDTpContactPtr contactWrapper, allContacts) {
        if (contactWrapper->isAvatarKnown()) {
            avatarIMAddresses << literalIMAddress(contactWrapper);
        }
        if (contactWrapper->isInformationKnown()) {
            infoIMAddresses << literalIMAddress(contactWrapper);
        }
    }

    CDTpQueryBuilder subBuilder;

    /* Delete Avatar for those who know it */
    if (!avatarIMAddresses.isEmpty()) {
        subBuilder = CDTpQueryBuilder("SyncContacts - remove known Avatar");
        static const QString tmpl = QString::fromLatin1(
                "?imAddress a nco:IMAddress.\n"
                "FILTER(?imAddress IN (%1))");
        subBuilder.appendRawSelection(tmpl.arg(avatarIMAddresses.join(filterInSeparator)));
        addRemoveAvatarToBuilder(subBuilder, imAddressVar);
        builder.appendRawQuery(subBuilder);
    }

    /* Delete ContactInfo for those who know it */
    if (!infoIMAddresses.isEmpty()) {
        subBuilder = CDTpQueryBuilder("SyncContacts - remove known ContactInfo");
        static const QString tmpl = QString::fromLatin1(
                "?imContact nco:hasAffiliation [ nco:hasIMAddress ?imAddress ].\n"
                "FILTER(?imAddress IN (%1))");
        subBuilder.appendRawSelection(tmpl.arg(infoIMAddresses.join(filterInSeparator)));
        addRemoveContactInfoToBuilder(subBuilder, imAddressVar, imContactVar);
        builder.appendRawQuery(subBuilder);
    }
}

void CDTpStorage::addRemoveContactsChangesToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        const QString &imContact,
        CDTpContact::Changes changes) const
{
    if (changes & CDTpContact::Alias) {
        debug() << "  alias changed";
        builder.deleteProperty(imAddress, "nco:imNickname");
    }
    if (changes & CDTpContact::Presence) {
        debug() << "  presence changed";
        addRemovePresenceToBuilder(builder, imAddress);
    }
    if (changes & CDTpContact::Capabilities) {
        debug() << "  capabilities changed";
        addRemoveCapabilitiesToBuilder(builder, imAddress);
    }
    if (changes & CDTpContact::Avatar) {
        debug() << "  avatar changed";
        addRemoveAvatarToBuilder(builder, imAddress);
    }
    if (changes & CDTpContact::Authorization) {
        debug() << "  authorization changed";
        builder.deleteProperty(imAddress, "nco:imAddressAuthStatusFrom");
        builder.deleteProperty(imAddress, "nco:imAddressAuthStatusTo");
    }
    if (changes & CDTpContact::Information) {
        debug() << "  vcard information changed";
        addRemoveContactInfoToBuilder(builder, imAddress, imContact);
    }

    builder.deleteProperty(imContact, "nie:contentLastModified");
}

void CDTpStorage::addContactChangesToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        const QString &imContact,
        CDTpContact::Changes changes,
        Tp::ContactPtr contact) const
{
    // Apply changes
    addContactChangesToBuilder(builder, imAddress, changes, contact);
    if (changes & CDTpContact::Information) {
        debug() << "  vcard information changed";
        addContactInfoToBuilder(builder, imAddress, imContact, contact);
    }

    builder.insertProperty(imContact, "nie:contentLastModified", literalTimeStamp());
}

void CDTpStorage::addContactChangesToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        CDTpContact::Changes changes,
        Tp::ContactPtr contact) const
{
    debug() << "Update contact" << imAddress;

    // Apply changes
    if (changes & CDTpContact::Alias) {
        debug() << "  alias changed";
        builder.insertProperty(imAddress, "nco:imNickname", literal(contact->alias()),
                privateGraph);
    }
    if (changes & CDTpContact::Presence) {
        debug() << "  presence changed";
        addPresenceToBuilder(builder, imAddress, contact->presence());
    }
    if (changes & CDTpContact::Capabilities) {
        debug() << "  capabilities changed";
        addCapabilitiesToBuilder(builder, imAddress, contact->capabilities());
    }
    if (changes & CDTpContact::Avatar) {
        debug() << "  avatar changed";
        addAvatarToBuilder(builder, imAddress, contact->avatarData().fileName);
    }
    if (changes & CDTpContact::Authorization) {
        debug() << "  authorization changed";
        builder.insertProperty(imAddress, "nco:imAddressAuthStatusFrom",
                presenceState(contact->subscriptionState()), privateGraph);
        builder.insertProperty(imAddress, "nco:imAddressAuthStatusTo",
                presenceState(contact->publishState()), privateGraph);
    }
}

void CDTpStorage::addRemovePresenceToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress) const
{
    builder.deleteProperty(imAddress, "nco:imPresence");
    builder.deleteProperty(imAddress, "nco:imStatusMessage");
    builder.deleteProperty(imAddress, "nco:presenceLastModified");
}

void CDTpStorage::addPresenceToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        const Tp::Presence &presence) const
{
    builder.insertProperty(imAddress, "nco:imPresence", presenceType(presence.type()),
            privateGraph);
    builder.insertProperty(imAddress, "nco:presenceLastModified", literalTimeStamp(),
            privateGraph);
    if (!presence.statusMessage().isEmpty()) {
        builder.insertProperty(imAddress, "nco:imStatusMessage",
                literal(presence.statusMessage()), privateGraph);
    }
}

void CDTpStorage::addRemoveCapabilitiesToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress) const
{
    builder.deleteProperty(imAddress, "nco:imCapability");
}

void CDTpStorage::addCapabilitiesToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        Tp::CapabilitiesBase capabilities) const
{
    if (capabilities.textChats()) {
        static const QString value = QString::fromLatin1("nco:im-capability-text-chat");
        builder.insertProperty(imAddress, "nco:imCapability", value, privateGraph);
    }
    if (capabilities.streamedMediaAudioCalls()) {
        static const QString value = QString::fromLatin1("nco:im-capability-audio-calls");
        builder.insertProperty(imAddress, "nco:imCapability", value, privateGraph);
    }
    if (capabilities.streamedMediaVideoCalls()) {
        static const QString value = QString::fromLatin1("nco:im-capability-video-calls");
        builder.insertProperty(imAddress, "nco:imCapability", value, privateGraph);
    }
}

void CDTpStorage::addRemoveAvatarToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress) const
{
    builder.deleteProperty(imAddress, "nco:imAvatar");
}

void CDTpStorage::addAvatarToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        const QString &fileName) const
{
    if (!fileName.isEmpty()) {
        static const QString tmpl = QString::fromLatin1("<%1>");
        const QString url = QUrl::fromLocalFile(fileName).toString();
        const QString dataObject = tmpl.arg(url);
        builder.createResource(dataObject, "nfo:FileDataObject", privateGraph);
        builder.insertProperty(dataObject, "nie:url", literal(url), privateGraph);
        builder.insertProperty(imAddress, "nco:imAvatar", dataObject, privateGraph);
    }
}

void CDTpStorage::addContactInfoToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        const QString &imContact,
        Tp::ContactPtr contact) const
{
    /* Use the imAddress as graph for ContactInfo fields, so we can easilly
     * know from which contact it comes from */
    const QString graph = imAddress;

    Tp::ContactInfoFieldList listContactInfo = contact->infoFields().allFields();
    if (listContactInfo.count() == 0) {
        debug() << "No contact info present";
        return;
    }

    QHash<QString, QString> affiliations;
    Q_FOREACH (const Tp::ContactInfoField &field, listContactInfo) {
        if (field.fieldValue.count() == 0) {
            continue;
        }

        /* FIXME:
         *  - Do we care about "fn" and "nickname" ?
         *  - How do we write affiliation for "org" ?
         */

        if (!field.fieldName.compare(QLatin1String("tel"))) {
            const QString voicePhoneNumber = literalContactInfo(field, 0);

            /* FIXME: Could change nco:PhoneNumber depending the the TYPE */
            CDTpQueryBuilder subBuilder("Insert Anon PhoneNumber");
            static const QString tmplQuery = QString::fromLatin1(
                    "FILTER(NOT EXISTS { ?resource a nco:PhoneNumber ; nco:phoneNumber %1 })");
            const QString phoneVar = subBuilder.uniquify("_:v");
            subBuilder.createResource(phoneVar, "nco:PhoneNumber");
            subBuilder.insertProperty(phoneVar, "nco:phoneNumber", voicePhoneNumber);
            subBuilder.insertProperty(phoneVar, "maemo:localPhoneNumber",
                    literal(qctNormalizePhoneNumber(field.fieldValue[0])));
            subBuilder.appendRawSelection(tmplQuery.arg(voicePhoneNumber));
            builder.prependRawQuery(subBuilder);

            /* save phone details to imContact */
            QString var = builder.uniquify("?phoneResource");
            static const QString tmplPhoneResource = QString::fromLatin1(
                "%1 nco:phoneNumber %2 .\n");
            const QString affiliation = ensureContactAffiliationToBuilder(builder, imContact, graph, field, affiliations);
            builder.appendRawSelectionInsert(tmplPhoneResource.arg(var).arg(voicePhoneNumber));
            builder.insertProperty(affiliation, "nco:hasPhoneNumber", var);
        }<commit_msg>Changes: Don't try to group ContactInfo INSERT, tracker is too weak for that<commit_after>/***************************************************************************
**
** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (<EMAIL>)
**
** This file is part of contactsd.
**
** If you have questions regarding the use of this file, please contact
** Nokia at <EMAIL>.
**
** This library is free software; you can redistribute it and/or
** modify it under the terms of the GNU Lesser General Public
** License version 2.1 as published by the Free Software Foundation
** and appearing in the file LICENSE.LGPL included in the packaging
** of this file.
**
****************************************************************************/

#include <TelepathyQt4/AvatarData>
#include <TelepathyQt4/ContactCapabilities>
#include <TelepathyQt4/ConnectionCapabilities>
#include <qtcontacts-tracker/phoneutils.h>

#include "cdtpstorage.h"
#include "debug.h"

using namespace Contactsd;

static const QString privateGraph = QString::fromLatin1("<urn:uuid:679293d4-60f0-49c7-8d63-f1528fe31f66>");
static const QString defaultGenerator = QString::fromLatin1("\"telepathy\"");
static const QString filterInSeparator = QString::fromLatin1(",");
static const QString defaultContactMe = QString::fromLatin1("nco:default-contact-me");
static const QString imAddressVar = QString::fromLatin1("?imAddress");
static const QString imContactVar = QString::fromLatin1("?imContact");
static const QString imAccountVar = QString::fromLatin1("?imAccount");

CDTpStorage::CDTpStorage(QObject *parent) : QObject(parent), mUpdateRunning(false)
{
    mUpdateTimer.setInterval(0);
    mUpdateTimer.setSingleShot(true);
    connect(&mUpdateTimer, SIGNAL(timeout()), SLOT(onUpdateQueueTimeout()));
}

CDTpStorage::~CDTpStorage()
{
}

void CDTpStorage::syncAccounts(const QList<CDTpAccountPtr> &accounts)
{
    CDTpQueryBuilder builder("SyncAccounts");

    /* Ensure the default-contact-me exists. NB#215973 */
    builder.createResource(defaultContactMe, "nco:PersonContact");

    QStringList imAccounts;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        imAccounts << literalIMAccount(accountWrapper);
    }

    /* Selection for all IMAccounts that does not exist anymore, and their IMAddresses */
    static const QString tmpl = QString::fromLatin1("?imAccount nco:hasIMContact ?imAddress.");
    QString selection = tmpl;
    if (!accounts.isEmpty()) {
        static const QString tmpl = QString::fromLatin1("\nFILTER (?imAccount NOT IN (%1)).");
        selection += tmpl.arg(imAccounts.join(filterInSeparator));
    }

    /* Purge contacts for accounts that does not exist anymore */
    CDTpQueryBuilder subBuilder("SyncAccounts - purge contacts");
    subBuilder.appendRawSelection(selection);
    addRemoveContactToBuilder(subBuilder, imAddressVar);
    builder.appendRawQuery(subBuilder);

    /* Purge accounts/imAddresses that does not exist anymore */
    subBuilder = CDTpQueryBuilder("SyncAccounts - purge imAddresses");
    subBuilder.appendRawSelection(selection);
    subBuilder.deleteResource(imAddressVar);
    builder.appendRawQuery(subBuilder);

    subBuilder = CDTpQueryBuilder("SyncAccounts - purge imAccount");
    subBuilder.appendRawSelection(selection);
    subBuilder.deleteResource(imAccountVar);
    builder.appendRawQuery(subBuilder);

    /* Sync accounts and their contacts */
    if (!accounts.isEmpty()) {
        subBuilder = CDTpQueryBuilder("SyncAccounts - sync accounts");
        addSyncAccountsToBuilder(subBuilder, accounts);
        builder.appendRawQuery(subBuilder);
    }

    /* Notify import progress for accounts that have contacts */
    QList<CDTpAccountPtr> rosterAccounts;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        if (!accountWrapper->contacts().isEmpty()) {
            rosterAccounts << accountWrapper;
            Q_EMIT syncStarted(accountWrapper);
        }
    }
    CDTpAccountsSparqlQuery *query = new CDTpAccountsSparqlQuery(rosterAccounts, builder, this);
    connect(query,
            SIGNAL(finished(CDTpSparqlQuery *)),
            SLOT(onSyncOperationEnded(CDTpSparqlQuery *)));
}

void CDTpStorage::syncAccount(CDTpAccountPtr accountWrapper)
{
    CDTpQueryBuilder builder("SyncAccount");

    /* Create account */
    QList<CDTpAccountPtr> accounts = QList<CDTpAccountPtr>() << accountWrapper;
    builder.updateProperty(defaultContactMe, "nie:contentLastModified", literalTimeStamp());
    addCreateAccountsToBuilder(builder, accounts);

    /* if account has no contacts, we are done */
    if (accountWrapper->contacts().isEmpty()) {
        new CDTpSparqlQuery(builder, this);
        return;
    }

    /* Create account's contacts */
    addCreateContactsToBuilder(builder, accountWrapper->contacts());

    /* Notify import progress */
    Q_EMIT syncStarted(accountWrapper);
    CDTpAccountsSparqlQuery *query = new CDTpAccountsSparqlQuery(accountWrapper, builder, this);
    connect(query,
            SIGNAL(finished(CDTpSparqlQuery *)),
            SLOT(onSyncOperationEnded(CDTpSparqlQuery *)));
}

void CDTpStorage::updateAccount(CDTpAccountPtr accountWrapper,
        CDTpAccount::Changes changes)
{
    debug() << "Update account" << literalIMAddress(accountWrapper);

    CDTpQueryBuilder builder("UpdateAccount");

    builder.updateProperty(defaultContactMe, "nie:contentLastModified", literalTimeStamp());

    addRemoveAccountsChangesToBuilder(builder,
        literalIMAddress(accountWrapper), literalIMAccount(accountWrapper),
        changes);
    addAccountChangesToBuilder(builder, accountWrapper, changes);

    new CDTpSparqlQuery(builder, this);
}

void CDTpStorage::removeAccount(CDTpAccountPtr accountWrapper)
{
    const QString imAccount = literalIMAccount(accountWrapper);
    debug() << "Remove account" << imAccount;

    /* Bind imAddress to all contacts of this imAccount */
    static const QString tmpl = QString::fromLatin1("%1 nco:hasIMContact ?imAddress.");
    const QString selection = tmpl.arg(imAccount);

    /* Remove account's contacts */
    CDTpQueryBuilder builder("RemoveAccount - contacts");
    builder.appendRawSelection(selection);
    addRemoveContactToBuilder(builder, imAddressVar);

    /* Remove imAddress */
    CDTpQueryBuilder subBuilder("RemoveAccount - imAddress");
    subBuilder.appendRawSelection(selection);
    subBuilder.deleteResource(imAddressVar);
    builder.appendRawQuery(subBuilder);

    /* Remove account */
    subBuilder = CDTpQueryBuilder("RemoveAccount - account");
    subBuilder.deleteResource(imAccount);
    builder.appendRawQuery(subBuilder);

    new CDTpSparqlQuery(builder, this);
}

void CDTpStorage::addSyncAccountsToBuilder(CDTpQueryBuilder &builder,
        const QList<CDTpAccountPtr> &accounts) const
{
    QStringList imAccounts;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        imAccounts << literalIMAccount(accountWrapper);
    }

    // Drop all mutable properties in all accounts at once, then (re-)create them
    static const QString tmpl = QString::fromLatin1(
            "?imAccount nco:imAccountAddress ?imAddress.\n"
            "FILTER(?imAccount IN (%1)).");
    builder.appendRawSelection(tmpl.arg(imAccounts.join(filterInSeparator)));

    addRemoveAccountsChangesToBuilder(builder, imAddressVar, imAccountVar, CDTpAccount::All);

    CDTpQueryBuilder subBuilder("CreateAccounts");

    subBuilder.updateProperty(defaultContactMe, "nie:contentLastModified", literalTimeStamp());
    addCreateAccountsToBuilder(subBuilder, accounts);
    builder.appendRawQuery(subBuilder);

    // Sync all contacts of all accounts. Special case for no-roster/offline
    // or disabled accounts.
    QList<CDTpAccountPtr> rosterAccounts;
    QList<CDTpAccountPtr> disabledAccounts;
    QList<CDTpAccountPtr> noRosterAccounts;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        if (accountWrapper->hasRoster()) {
            rosterAccounts << accountWrapper;
        } else if (!accountWrapper->account()->isEnabled()) {
            disabledAccounts << accountWrapper;
        } else {
            noRosterAccounts << accountWrapper;
        }
    }

    if (!rosterAccounts.isEmpty()) {
        subBuilder = CDTpQueryBuilder("SyncRosterAccounts");
        addSyncRosterAccountsContactsToBuilder(subBuilder, rosterAccounts);
        builder.appendRawQuery(subBuilder);
    }

    if (!disabledAccounts.isEmpty()) {
        subBuilder = CDTpQueryBuilder("SyncDisabledAccounts");
        addSyncDisabledAccountsContactsToBuilder(subBuilder, disabledAccounts);
        builder.appendRawQuery(subBuilder);
    }

    if (!noRosterAccounts.isEmpty()) {
        subBuilder = CDTpQueryBuilder("SyncNoRosterAccounts");
        addSyncNoRosterAccountsContactsToBuilder(subBuilder, noRosterAccounts);
        builder.appendRawQuery(subBuilder);
    }
}

void CDTpStorage::addCreateAccountsToBuilder(CDTpQueryBuilder &builder,
        const QList<CDTpAccountPtr> &accounts) const
{
    QStringList imAddresses;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        Tp::AccountPtr account = accountWrapper->account();
        const QString imAccount = literalIMAccount(accountWrapper);
        const QString imAddress = literalIMAddress(accountWrapper);

        debug() << "Create account" << imAddress;

        // Ensure the IMAccount exists
        builder.createResource(imAccount, "nco:IMAccount", privateGraph);
        builder.insertProperty(imAccount, "nco:imAccountType", literal(account->protocolName()), privateGraph);
        builder.insertProperty(imAccount, "nco:imAccountAddress", imAddress, privateGraph);
        builder.insertProperty(imAccount, "nco:hasIMContact", imAddress, privateGraph);

        // Ensure the self contact has an IMAddress
        builder.createResource(imAddress, "nco:IMAddress", privateGraph);
        builder.insertProperty(imAddress, "nco:imID", literal(account->normalizedName()), privateGraph);
        builder.insertProperty(imAddress, "nco:imProtocol", literal(account->protocolName()), privateGraph);

        // Add all mutable properties
        addAccountChangesToBuilder(builder, accountWrapper, CDTpAccount::All);

        imAddresses << imAddress;
    }

    // Ensure the IMAddresses are bound to the default-contact-me via an affiliation
    static const QString tmpl = QString::fromLatin1(
        "INSERT {\n"
        "    GRAPH %1 {\n"
        "        nco:default-contact-me nco:hasAffiliation _:affiliation.\n"
        "        _:affiliation a nco:Affiliation;\n"
        "                      nco:hasIMAddress ?imAddress;\n"
        "                      rdfs:label \"Other\".\n"
        "    }\n"
        "}\n"
        "WHERE {\n"
        "    ?imAddress a nco:IMAddress.\n"
        "    FILTER (?imAddress IN (%2) &&\n"
        "            NOT EXISTS { nco:default-contact-me nco:hasAffiliation [ nco:hasIMAddress ?imAddress ] })\n"
        "}\n");
    builder.appendRawQuery(tmpl.arg(privateGraph).arg(imAddresses.join(filterInSeparator)));
}

void CDTpStorage::addRemoveAccountsChangesToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        const QString &imAccount,
        CDTpAccount::Changes changes) const
{
    if (changes & CDTpAccount::Presence) {
        addRemovePresenceToBuilder(builder, imAddress);
    }
    if (changes & CDTpAccount::Avatar) {
        addRemoveAvatarToBuilder(builder, imAddress);
    }
    if (changes & CDTpAccount::Nickname) {
        builder.deleteProperty(imAddress, "nco:imNickname");
    }
    if (changes & CDTpAccount::DisplayName) {
        builder.deleteProperty(imAccount, "nco:imDisplayName");
    }
}

void CDTpStorage::addAccountChangesToBuilder(CDTpQueryBuilder &builder,
        CDTpAccountPtr accountWrapper,
        CDTpAccount::Changes changes) const
{
    Tp::AccountPtr account = accountWrapper->account();
    const QString imAccount = literalIMAccount(accountWrapper);
    const QString imAddress = literalIMAddress(accountWrapper);

    if (changes & CDTpAccount::Presence) {
        debug() << "  presence changed";
        addPresenceToBuilder(builder, imAddress, account->currentPresence());
    }
    if (changes & CDTpAccount::Avatar) {
        debug() << "  avatar changed";
        addAvatarToBuilder(builder, imAddress, saveAccountAvatar(accountWrapper));
    }
    if (changes & CDTpAccount::Nickname) {
        debug() << "  nickname changed";
        builder.insertProperty(imAddress, "nco:imNickname", literal(account->nickname()),
                privateGraph);
    }
    if (changes & CDTpAccount::DisplayName) {
        debug() << "  display name changed";
        builder.insertProperty(imAccount, "nco:imDisplayName", literal(account->displayName()),
                privateGraph);
    }
}

QString CDTpStorage::saveAccountAvatar(CDTpAccountPtr accountWrapper) const
{
    const Tp::Avatar &avatar = accountWrapper->account()->avatar();

    if (avatar.avatarData.isEmpty()) {
        return QString();
    }

    static const QString tmpl = QString::fromLatin1("%1/.contacts/avatars/%2");
    QString fileName = tmpl.arg(QDir::homePath())
        .arg(QLatin1String(QCryptographicHash::hash(avatar.avatarData, QCryptographicHash::Sha1).toHex()));
    debug() << "Saving account avatar to" << fileName;

    QFile avatarFile(fileName);
    if (!avatarFile.open(QIODevice::WriteOnly)) {
        warning() << "Unable to save account avatar: error opening avatar "
            "file" << fileName << "for writing";
        return QString();
    }
    avatarFile.write(avatar.avatarData);
    avatarFile.close();

    return fileName;
}

void CDTpStorage::syncAccountContacts(CDTpAccountPtr accountWrapper)
{
    CDTpQueryBuilder builder("SyncAccountContacts");

    QList<CDTpAccountPtr> accounts = QList<CDTpAccountPtr>() << accountWrapper;
    if (accountWrapper->hasRoster()) {
        addSyncRosterAccountsContactsToBuilder(builder, accounts);
    } else if (!accountWrapper->account()->isEnabled()) {
        addSyncDisabledAccountsContactsToBuilder(builder, accounts);
    } else {
        addSyncNoRosterAccountsContactsToBuilder(builder, accounts);
    }

    /* if account has no contacts, we are done */
    if (accountWrapper->contacts().isEmpty()) {
        new CDTpSparqlQuery(builder, this);
        return;
    }

    /* Notify import progress */
    Q_EMIT syncStarted(accountWrapper);
    CDTpAccountsSparqlQuery *query = new CDTpAccountsSparqlQuery(accountWrapper, builder, this);
    connect(query,
            SIGNAL(finished(CDTpSparqlQuery *)),
            SLOT(onSyncOperationEnded(CDTpSparqlQuery *)));
}

void CDTpStorage::syncAccountContacts(CDTpAccountPtr accountWrapper,
        const QList<CDTpContactPtr> &contactsAdded,
        const QList<CDTpContactPtr> &contactsRemoved)
{
    CDTpQueryBuilder builder("Contacts Added/Removed");

    if (!contactsAdded.isEmpty()) {
        // delete the timestamp from PersonContact linked to imAddresses
        // (unlikely to exist). addCreateContactsToBuilder() will insert a new
        // value.
        QStringList imAddresses;
        Q_FOREACH (const CDTpContactPtr &contactWrapper, contactsAdded) {
            imAddresses << literalIMAddress(contactWrapper);
        }
        static const QString tmpl = QString::fromLatin1(
                "?imContact nco:hasAffiliation [ nco:hasIMAddress ?imAddress ].\n"
                "FILTER(?imAddress IN (%1)).");
        builder.appendRawSelection(tmpl.arg(imAddresses.join(filterInSeparator)));
        builder.deleteProperty(imContactVar, "nie:contentLastModified");

        // Create contacts
        CDTpQueryBuilder subBuilder("Contacts Added/Removed - Create contacts");
        addCreateContactsToBuilder(subBuilder, contactsAdded);
        builder.appendRawQuery(subBuilder);
    }

    if (!contactsRemoved.isEmpty()) {
        addRemoveContactsToBuilder(builder, accountWrapper, contactsRemoved);
    }

    new CDTpSparqlQuery(builder, this);
}

/* Use this only in offline mode - use syncAccountContacts in online mode */
void CDTpStorage::removeAccountContacts(const QString &accountPath, const QStringList &contactIds)
{
    CDTpQueryBuilder builder;
    addRemoveContactsToBuilder(builder, accountPath, contactIds);
    new CDTpSparqlQuery(builder, this);
}

void  CDTpStorage::contactsToAvoid(const QString &accountPath, const QStringList &contactIds)
{
   mContactsToAvoid[accountPath] = contactIds;
}

void CDTpStorage::updateContact(CDTpContactPtr contactWrapper, CDTpContact::Changes changes)
{
    if (!mUpdateQueue.contains(contactWrapper)) {
        mUpdateQueue.insert(contactWrapper, changes);
    } else {
        mUpdateQueue[contactWrapper] |= changes;
    }

    if (!mUpdateRunning) {
        mUpdateTimer.start();
    }
}

void CDTpStorage::onUpdateQueueTimeout()
{
    debug() << "Update" << mUpdateQueue.count() << "contacts";

    bool found = false;
    CDTpQueryBuilder builder("UpdateContacts");
    QHash<CDTpContactPtr, CDTpContact::Changes>::const_iterator i;
    for (i = mUpdateQueue.constBegin(); i != mUpdateQueue.constEnd(); i++) {
        CDTpContactPtr contactWrapper = i.key();
        CDTpContact::Changes changes = i.value();
        if (!contactWrapper->isVisible()) {
            continue;
        }

        CDTpQueryBuilder subBuilder("UpdateContact");

        // bind imContact to imAddresses
        static const QString tmpl = QString::fromLatin1(
                "?imContact nco:hasAffiliation [ nco:hasIMAddress %1 ].");

        const QString imAddress = literalIMAddress(contactWrapper);
        subBuilder.appendRawSelection(tmpl.arg(imAddress));
        addRemoveContactsChangesToBuilder(subBuilder, imAddress, imContactVar, changes);
        addContactChangesToBuilder(subBuilder, imAddress, imContactVar, changes, contactWrapper->contact());
        builder.appendRawQuery(subBuilder);

        found = true;
    }
    mUpdateQueue.clear();

    if (!found) {
        debug() << "  None needs update";
        return;
    }

    mUpdateRunning = true;
    CDTpSparqlQuery *query = new CDTpSparqlQuery(builder, this);
    connect(query,
            SIGNAL(finished(CDTpSparqlQuery *)),
            SLOT(onUpdateFinished(CDTpSparqlQuery *)));
}

void CDTpStorage::onUpdateFinished(CDTpSparqlQuery *query)
{
    Q_UNUSED(query);

    mUpdateRunning = false;
    if (!mUpdateQueue.isEmpty()) {
        mUpdateTimer.start();
    }
}

void CDTpStorage::addSyncNoRosterAccountsContactsToBuilder(CDTpQueryBuilder &builder,
        const QList<CDTpAccountPtr> accounts) const
{
    QStringList imAccounts;
    QStringList imAddresses;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        const QString imAddress = literalIMAddress(accountWrapper);
        const QString imAccount = literalIMAccount(accountWrapper);

        debug() << "Sync no roster account" << imAddress;

        imAddresses << imAddress;
        imAccounts << imAccount;
    }

    /* Bind ?imAccount to all no roster accounts.
     * Bind ?imAddress to all accounts' im contacts, except for the self contact
     * because we know it has presence OFFLINE, and that's done when updating
     * account.
     * Bind ?imContact to all contacts' local contact.
     */
    static const QString tmpl = QString::fromLatin1(
            "?imAccount nco:hasIMContact ?imAddress.\n"
            "?imContact nco:hasAffiliation [ nco:hasIMAddress ?imAddress ].\n"
            "FILTER(?imAddress NOT IN (%1) && ?imAccount IN (%2)).");
    builder.appendRawSelection(tmpl.arg(imAddresses.join(filterInSeparator)).arg(imAccounts.join(filterInSeparator)));

    // FIXME: we could use addPresenceToBuilder() somehow
    builder.updateProperty(imAddressVar, "nco:imPresence", presenceType(Tp::ConnectionPresenceTypeUnknown),
            privateGraph);
    builder.updateProperty(imAddressVar, "nco:presenceLastModified", literalTimeStamp(),
            privateGraph);
    builder.updateProperty(imContactVar, "nie:contentLastModified", literalTimeStamp());

    // Update capabilities of all contacts, since we don't know them anymore,
    // reset them to the account's caps.
    addRemoveCapabilitiesToBuilder(builder, imAddressVar);

    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        addCapabilitiesToBuilder(builder, literalIMAddress(accountWrapper),
                accountWrapper->account()->capabilities());
    }
}

void CDTpStorage::addSyncDisabledAccountsContactsToBuilder(CDTpQueryBuilder &builder,
        const QList<CDTpAccountPtr> accounts) const
{
    // FIXME: disabled for now
    addSyncNoRosterAccountsContactsToBuilder(builder, accounts);
}

void CDTpStorage::addSyncRosterAccountsContactsToBuilder(CDTpQueryBuilder &builder,
        const QList<CDTpAccountPtr> &accounts) const
{
    /* Remove all mutable info from all contacts of those accounts */
    addRemoveContactsChangesToBuilder(builder, accounts);

    /* Delete the hasIMContact property on IMAccounts (except for self contact)
     * then sync all contacts (that will add back hasIMContact for them). After
     * that we can purge all imAddresses not bound to an IMAccount anymore. */
    QStringList imAccounts;
    QStringList imAddresses;
    QList<CDTpContactPtr> allContacts;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        const QString imAddress = literalIMAddress(accountWrapper);
        const QString imAccount = literalIMAccount(accountWrapper);
        const QString accountPath = accountWrapper->account()->objectPath();

        debug() << "Sync roster account" << imAddress;

        imAccounts << imAccount;
        imAddresses << imAddress;
        Q_FOREACH(CDTpContactPtr contactPtr, accountWrapper->contacts()) {
            QStringList offlineRemoved = mContactsToAvoid[accountPath];
            if (!offlineRemoved.contains(contactPtr->contact()->id())) {
                allContacts << contactPtr;
            }
        }
    }
    CDTpQueryBuilder subBuilder = CDTpQueryBuilder("SyncRosterAccounts - drop hasIMContact");
    static const QString tmpl = QString::fromLatin1(
            "?imAccount nco:hasIMContact ?imAddress.\n"
            "FILTER(?imAddress NOT IN (%1) && ?imAccount IN (%2)).");
    subBuilder.appendRawSelection(tmpl.arg(imAddresses.join(filterInSeparator)).arg(imAccounts.join(filterInSeparator)));
    subBuilder.deleteProperty(imAccountVar, "nco:hasIMContact", imAddressVar);
    builder.appendRawQuery(subBuilder);

    /* Now create all contacts */
    if (!allContacts.isEmpty()) {
        subBuilder = CDTpQueryBuilder("CreateContacts");
        addCreateContactsToBuilder(subBuilder, allContacts);
        builder.appendRawQuery(subBuilder);
    }

    /* Bind imAddress to all contacts that does not exist anymore, to purge them */
    static const QString selection = QString::fromLatin1(
            "GRAPH %1 { ?imAddress a nco:IMAddress }.\n"
            "FILTER(NOT EXISTS { ?imAccount nco:hasIMContact ?imAddress }).")
            .arg(privateGraph);

    subBuilder = CDTpQueryBuilder("SyncRosterAccounts - purge contacts");
    subBuilder.appendRawSelection(selection);
    addRemoveContactToBuilder(subBuilder, imAddressVar);
    builder.appendRawQuery(subBuilder);

    subBuilder = CDTpQueryBuilder("SyncRosterAccounts - purge imAddresses");
    subBuilder.appendRawSelection(selection);
    subBuilder.deleteResource(imAddressVar);
    builder.appendRawQuery(subBuilder);
}

void CDTpStorage::addCreateContactsToBuilder(CDTpQueryBuilder &builder,
        const QList<CDTpContactPtr> &contacts) const
{
    // Ensure all imAddress exists and are linked from imAccount
    QStringList imAddresses;
    Q_FOREACH (const CDTpContactPtr contactWrapper, contacts) {
        CDTpAccountPtr accountWrapper = contactWrapper->accountWrapper();

        const QString imAddress = literalIMAddress(contactWrapper);
        builder.createResource(imAddress, "nco:IMAddress", privateGraph);
        builder.insertProperty(imAddress, "nco:imID", literal(contactWrapper->contact()->id()),
                privateGraph);
        builder.insertProperty(imAddress, "nco:imProtocol", literal(accountWrapper->account()->protocolName()),
                privateGraph);

        const QString imAccount = literalIMAccount(accountWrapper);
        builder.insertProperty(imAccount, "nco:hasIMContact", imAddress, privateGraph);

        // Add mutable properties except for ContactInfo
        addContactChangesToBuilder(builder, imAddress, CDTpContact::All,
                contactWrapper->contact());

        imAddresses << imAddress;
    }

    // Ensure all imAddresses are bound to a PersonContact
    static const QString tmpl = QString::fromLatin1(
        "INSERT {\n"
        "    GRAPH %1 {\n"
        "        _:contact a nco:PersonContact;\n"
        "                  nie:contentCreated %3;\n"
        "                  nie:generator %4.\n"
        "    }\n"
        "    GRAPH %2 {\n"
        "        _:contact nco:hasAffiliation _:affiliation.\n"
        "        _:affiliation a nco:Affiliation;\n"
        "                      nco:hasIMAddress ?imAddress;\n"
        "                      rdfs:label \"Other\".\n"
        "    }\n"
        "}\n"
        "WHERE {\n"
        "    GRAPH %2 { ?imAddress a nco:IMAddress }.\n"
        "    FILTER (NOT EXISTS { ?imContact nco:hasAffiliation [ nco:hasIMAddress ?imAddress ] })\n"
        "}\n");
    builder.appendRawQuery(tmpl.arg(CDTpQueryBuilder::defaultGraph).arg(privateGraph)
            .arg(literalTimeStamp()).arg(defaultGenerator));

    // Insert timestamp on all imContact bound to those imAddresses
    CDTpQueryBuilder subBuilder("CreateContacts - timestamp");
    static const QString tmpl2 = QString::fromLatin1(
            "?imContact nco:hasAffiliation [ nco:hasIMAddress ?imAddress ].\n"
            "FILTER(?imAddress IN (%1)).");
    subBuilder.appendRawSelection(tmpl2.arg(imAddresses.join(filterInSeparator)));
    subBuilder.insertProperty(imContactVar, "nie:contentLastModified", literalTimeStamp());
    builder.appendRawQuery(subBuilder);

    // Add ContactInfo seperately because we need to bind to the PersonContact
    // and each phone number. that makes things much slower.
    Q_FOREACH (const CDTpContactPtr contactWrapper, contacts) {
        if (!contactWrapper->isInformationKnown()) {
            continue;
        }

        const QString imAddress = literalIMAddress(contactWrapper);
        const QString imContact = subBuilder.uniquify("?imContact");
        static const QString tmpl = QString::fromLatin1("%1 nco:hasAffiliation [ nco:hasIMAddress %2 ].");
        subBuilder = CDTpQueryBuilder("CreateContacts - add ContactInfo");
        subBuilder.appendRawSelection(tmpl.arg(imContact).arg(imAddress));
        addContactInfoToBuilder(subBuilder, imAddress, imContact, contactWrapper->contact());
        builder.appendRawQuery(subBuilder);
    }
}

void CDTpStorage::addRemoveContactsChangesToBuilder(CDTpQueryBuilder &builder,
    const QList<CDTpAccountPtr> &accounts) const
{
    QStringList imAccounts;
    QStringList imAddresses;
    QList<CDTpContactPtr> allContacts;
    Q_FOREACH (const CDTpAccountPtr &accountWrapper, accounts) {
        imAccounts << literalIMAccount(accountWrapper);
        imAddresses << literalIMAddress(accountWrapper);
        allContacts << accountWrapper->contacts();
    }

    if (allContacts.isEmpty()) {
        return;
    }

    /* We can easily drop all mutable properties at once for all contacts of
     * all those accounts. BUT Avatar and ContactInfo are special because they
     * could not be known yet in which case we want to keep the current value.
     * so we delete Avatar/Information later only for the contacts that already
     * have it. */

    uint mandatoryFlags = CDTpContact::All;
    mandatoryFlags &= ~CDTpContact::Information;
    mandatoryFlags &= ~CDTpContact::Avatar;
    static const QString tmpl = QString::fromLatin1(
            "?imAccount nco:hasIMContact ?imAddress.\n"
            "?imContact nco:hasAffiliation [ nco:hasIMAddress ?imAddress ].\n"
            "FILTER(?imAddress NOT IN (%1) && ?imAccount IN (%2)).");
    builder.appendRawSelection(tmpl.arg(imAddresses.join(filterInSeparator)).arg(imAccounts.join(filterInSeparator)));
    addRemoveContactsChangesToBuilder(builder, imAddressVar, imContactVar, CDTpContact::Changes(mandatoryFlags));

    QStringList avatarIMAddresses;
    QStringList infoIMAddresses;
    Q_FOREACH (const CDTpContactPtr contactWrapper, allContacts) {
        if (contactWrapper->isAvatarKnown()) {
            avatarIMAddresses << literalIMAddress(contactWrapper);
        }
        if (contactWrapper->isInformationKnown()) {
            infoIMAddresses << literalIMAddress(contactWrapper);
        }
    }

    CDTpQueryBuilder subBuilder;

    /* Delete Avatar for those who know it */
    if (!avatarIMAddresses.isEmpty()) {
        subBuilder = CDTpQueryBuilder("SyncContacts - remove known Avatar");
        static const QString tmpl = QString::fromLatin1(
                "?imAddress a nco:IMAddress.\n"
                "FILTER(?imAddress IN (%1))");
        subBuilder.appendRawSelection(tmpl.arg(avatarIMAddresses.join(filterInSeparator)));
        addRemoveAvatarToBuilder(subBuilder, imAddressVar);
        builder.appendRawQuery(subBuilder);
    }

    /* Delete ContactInfo for those who know it */
    if (!infoIMAddresses.isEmpty()) {
        subBuilder = CDTpQueryBuilder("SyncContacts - remove known ContactInfo");
        static const QString tmpl = QString::fromLatin1(
                "?imContact nco:hasAffiliation [ nco:hasIMAddress ?imAddress ].\n"
                "FILTER(?imAddress IN (%1))");
        subBuilder.appendRawSelection(tmpl.arg(infoIMAddresses.join(filterInSeparator)));
        addRemoveContactInfoToBuilder(subBuilder, imAddressVar, imContactVar);
        builder.appendRawQuery(subBuilder);
    }
}

void CDTpStorage::addRemoveContactsChangesToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        const QString &imContact,
        CDTpContact::Changes changes) const
{
    if (changes & CDTpContact::Alias) {
        debug() << "  alias changed";
        builder.deleteProperty(imAddress, "nco:imNickname");
    }
    if (changes & CDTpContact::Presence) {
        debug() << "  presence changed";
        addRemovePresenceToBuilder(builder, imAddress);
    }
    if (changes & CDTpContact::Capabilities) {
        debug() << "  capabilities changed";
        addRemoveCapabilitiesToBuilder(builder, imAddress);
    }
    if (changes & CDTpContact::Avatar) {
        debug() << "  avatar changed";
        addRemoveAvatarToBuilder(builder, imAddress);
    }
    if (changes & CDTpContact::Authorization) {
        debug() << "  authorization changed";
        builder.deleteProperty(imAddress, "nco:imAddressAuthStatusFrom");
        builder.deleteProperty(imAddress, "nco:imAddressAuthStatusTo");
    }
    if (changes & CDTpContact::Information) {
        debug() << "  vcard information changed";
        addRemoveContactInfoToBuilder(builder, imAddress, imContact);
    }

    builder.deleteProperty(imContact, "nie:contentLastModified");
}

void CDTpStorage::addContactChangesToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        const QString &imContact,
        CDTpContact::Changes changes,
        Tp::ContactPtr contact) const
{
    // Apply changes
    addContactChangesToBuilder(builder, imAddress, changes, contact);
    if (changes & CDTpContact::Information) {
        debug() << "  vcard information changed";
        addContactInfoToBuilder(builder, imAddress, imContact, contact);
    }

    builder.insertProperty(imContact, "nie:contentLastModified", literalTimeStamp());
}

void CDTpStorage::addContactChangesToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        CDTpContact::Changes changes,
        Tp::ContactPtr contact) const
{
    debug() << "Update contact" << imAddress;

    // Apply changes
    if (changes & CDTpContact::Alias) {
        debug() << "  alias changed";
        builder.insertProperty(imAddress, "nco:imNickname", literal(contact->alias()),
                privateGraph);
    }
    if (changes & CDTpContact::Presence) {
        debug() << "  presence changed";
        addPresenceToBuilder(builder, imAddress, contact->presence());
    }
    if (changes & CDTpContact::Capabilities) {
        debug() << "  capabilities changed";
        addCapabilitiesToBuilder(builder, imAddress, contact->capabilities());
    }
    if (changes & CDTpContact::Avatar) {
        debug() << "  avatar changed";
        addAvatarToBuilder(builder, imAddress, contact->avatarData().fileName);
    }
    if (changes & CDTpContact::Authorization) {
        debug() << "  authorization changed";
        builder.insertProperty(imAddress, "nco:imAddressAuthStatusFrom",
                presenceState(contact->subscriptionState()), privateGraph);
        builder.insertProperty(imAddress, "nco:imAddressAuthStatusTo",
                presenceState(contact->publishState()), privateGraph);
    }
}

void CDTpStorage::addRemovePresenceToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress) const
{
    builder.deleteProperty(imAddress, "nco:imPresence");
    builder.deleteProperty(imAddress, "nco:imStatusMessage");
    builder.deleteProperty(imAddress, "nco:presenceLastModified");
}

void CDTpStorage::addPresenceToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        const Tp::Presence &presence) const
{
    builder.insertProperty(imAddress, "nco:imPresence", presenceType(presence.type()),
            privateGraph);
    builder.insertProperty(imAddress, "nco:presenceLastModified", literalTimeStamp(),
            privateGraph);
    if (!presence.statusMessage().isEmpty()) {
        builder.insertProperty(imAddress, "nco:imStatusMessage",
                literal(presence.statusMessage()), privateGraph);
    }
}

void CDTpStorage::addRemoveCapabilitiesToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress) const
{
    builder.deleteProperty(imAddress, "nco:imCapability");
}

void CDTpStorage::addCapabilitiesToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        Tp::CapabilitiesBase capabilities) const
{
    if (capabilities.textChats()) {
        static const QString value = QString::fromLatin1("nco:im-capability-text-chat");
        builder.insertProperty(imAddress, "nco:imCapability", value, privateGraph);
    }
    if (capabilities.streamedMediaAudioCalls()) {
        static const QString value = QString::fromLatin1("nco:im-capability-audio-calls");
        builder.insertProperty(imAddress, "nco:imCapability", value, privateGraph);
    }
    if (capabilities.streamedMediaVideoCalls()) {
        static const QString value = QString::fromLatin1("nco:im-capability-video-calls");
        builder.insertProperty(imAddress, "nco:imCapability", value, privateGraph);
    }
}

void CDTpStorage::addRemoveAvatarToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress) const
{
    builder.deleteProperty(imAddress, "nco:imAvatar");
}

void CDTpStorage::addAvatarToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        const QString &fileName) const
{
    if (!fileName.isEmpty()) {
        static const QString tmpl = QString::fromLatin1("<%1>");
        const QString url = QUrl::fromLocalFile(fileName).toString();
        const QString dataObject = tmpl.arg(url);
        builder.createResource(dataObject, "nfo:FileDataObject", privateGraph);
        builder.insertProperty(dataObject, "nie:url", literal(url), privateGraph);
        builder.insertProperty(imAddress, "nco:imAvatar", dataObject, privateGraph);
    }
}

void CDTpStorage::addContactInfoToBuilder(CDTpQueryBuilder &builder,
        const QString &imAddress,
        const QString &imContact,
        Tp::ContactPtr contact) const
{
    /* Use the imAddress as graph for ContactInfo fields, so we can easilly
     * know from which contact it comes from */
    const QString graph = imAddress;

    Tp::ContactInfoFieldList listContactInfo = contact->infoFields().allFields();
    if (listContactInfo.count() == 0) {
        debug() << "No contact info present";
        return;
    }

    QHash<QString, QString> affiliations;
    Q_FOREACH (const Tp::ContactInfoField &field, listContactInfo) {
        if (field.fieldValue.count() == 0) {
            continue;
        }

        /* FIXME:
         *  - Do we care about "fn" and "nickname" ?
         *  - How do we write affiliation for "org" ?
         */

        if (!field.fieldName.compare(QLatin1String("tel"))) {
            const QString voicePhoneNumber = literalContactInfo(field, 0);

            /* FIXME: Could change nco:PhoneNumber depending the the TYPE */
            CDTpQueryBuilder subBuilder("Insert Anon PhoneNumber");
            static const QString tmplQuery = QString::fromLatin1(
                    "FILTER(NOT EXISTS { ?resource a nco:PhoneNumber ; nco:phoneNumber %1 })");
            const QString phoneVar = subBuilder.uniquify("_:v");
            subBuilder.createResource(phoneVar, "nco:PhoneNumber");
            subBuilder.insertProperty(phoneVar, "nco:phoneNumber", voicePhoneNumber);
            subBuilder.insertProperty(phoneVar, "maemo:localPhoneNumber",
                    literal(qctNormalizePhoneNumber(field.fieldValue[0])));
            subBuilder.appendRawSelection(tmplQuery.arg(voicePhoneNumber));
            builder.prependRawQuery(subBuilder);

            /* save phone details to imContact */
            QString var = builder.uniquify("?phoneResource");
            static const QString tmplPhoneResource = QString::fromLatin1(
                "%1 nco:phoneNumber %2.");
            const QString affiliation = ensureContactAffiliationToBuilder(builder, imContact, graph, field, affiliations);
            builder.appendRawSelectionInsert(tmplPhoneResource.arg(var).arg(voicePhoneNumber));
            builder.insertProperty(affiliation, "nco:hasPhoneNumber", var);
        }
{'fraction_non_alphanumeric': 0.1102222454226317, 'fraction_numerical': 0.0029232515953906586, 'mean_word_length': 3.636534156229201, 'pattern_counts': {'":': 0, '<': 299, '<?xml version=': 0, '>': 157, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '226474', 'n_tokens_mistral': 21951, 'n_tokens_neox': 20896, 'n_words': 4473}