Commit 7f57278a authored by David Jarvie's avatar David Jarvie
Browse files

Implement generic calendar updater class

parent 9853913f
......@@ -107,6 +107,8 @@ set(kalarm_bin_SRCS ${kalarm_bin_SRCS}
resources/akonadiresource.cpp
resources/akonadiresourcecreator.cpp
resources/akonadiresourcemigrator.cpp
resources/calendarupdater.cpp
resources/akonadicalendarupdater.cpp
kalarmmigrateapplication.cpp
akonadicollectionsearch.cpp
eventid.cpp
......
/*
* akonadicalendarupdater.cpp - updates a calendar to current KAlarm format
* Program: kalarm
* Copyright © 2011-2020 David Jarvie <djarvie@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "akonadicalendarupdater.h"
#include "kalarmsettings.h"
#include "kalarmdirsettings.h"
#include "resources/akonadidatamodel.h"
#include "resources/akonadiresource.h"
#include "resources/resources.h"
#include "lib/messagebox.h"
#include "kalarm_debug.h"
#include <KAlarmCal/CollectionAttribute>
#include <KAlarmCal/CompatibilityAttribute>
#include <KAlarmCal/Version>
#include <AkonadiCore/AgentManager>
#include <KLocalizedString>
#include <QTimer>
using namespace Akonadi;
using namespace KAlarmCal;
AkonadiCalendarUpdater::AkonadiCalendarUpdater(const Collection& collection, bool dirResource,
bool ignoreKeepFormat, bool newCollection, QObject* parent, QWidget* promptParent)
: CalendarUpdater(collection.id(), ignoreKeepFormat, parent, promptParent)
, mCollection(collection)
, mDirResource(dirResource)
, mNewCollection(newCollection)
{
}
/******************************************************************************
* If an existing Akonadi resource calendar can be converted to the current
* KAlarm format, prompt the user whether to convert it, and if yes, tell the
* Akonadi resource to update the backend storage to the current format.
* The CollectionAttribute's KeepFormat property will be updated if the user
* chooses not to update the calendar.
*
* Note: the collection should be up to date: use AkonadiDataModel::refresh()
* before calling this function.
*/
void AkonadiCalendarUpdater::updateToCurrentFormat(const Resource& resource, bool ignoreKeepFormat, QObject* parent)
{
qCDebug(KALARM_LOG) << "AkonadiCalendarUpdater::updateToCurrentFormat:" << resource.id();
if (containsResource(resource.id()))
return; // prevent multiple simultaneous user prompts
const AgentInstance agent = AgentManager::self()->instance(resource.configName());
const QString id = agent.type().identifier();
bool dirResource;
if (id == AkonadiResource::KALARM_RESOURCE)
dirResource = false;
else if (id == AkonadiResource::KALARM_DIR_RESOURCE)
dirResource = true;
else
{
qCCritical(KALARM_LOG) << "AkonadiCalendarUpdater::updateToCurrentFormat: Invalid agent type" << id;
return;
}
const Collection& collection = AkonadiResource::collection(resource);
AkonadiCalendarUpdater* updater = new AkonadiCalendarUpdater(collection, dirResource, ignoreKeepFormat, false, parent, qobject_cast<QWidget*>(parent));
QTimer::singleShot(0, updater, &AkonadiCalendarUpdater::update);
}
/******************************************************************************
* If the calendar is not in the current KAlarm format, prompt the user whether
* to convert to the current format, and then perform the conversion.
*/
bool AkonadiCalendarUpdater::update()
{
qCDebug(KALARM_LOG) << "AkonadiCalendarUpdater::update:" << mCollection.id() << (mDirResource ? "directory" : "file");
bool result = true;
if (isDuplicate())
qCDebug(KALARM_LOG) << "AkonadiCalendarUpdater::update: Not updating (concurrent update in progress)";
else if (mCollection.hasAttribute<CompatibilityAttribute>()) // must know format to update
{
const CompatibilityAttribute* compatAttr = mCollection.attribute<CompatibilityAttribute>();
const KACalendar::Compat compatibility = compatAttr->compatibility();
qCDebug(KALARM_LOG) << "AkonadiCalendarUpdater::update: current format:" << compatibility;
if ((compatibility & ~KACalendar::Converted)
// The calendar isn't in the current KAlarm format
&& !(compatibility & ~(KACalendar::Convertible | KACalendar::Converted)))
{
// The calendar format is convertible to the current KAlarm format
if (!mIgnoreKeepFormat
&& mCollection.hasAttribute<CollectionAttribute>()
&& mCollection.attribute<CollectionAttribute>()->keepFormat())
qCDebug(KALARM_LOG) << "AkonadiCalendarUpdater::update: Not updating format (previous user choice)";
else
{
// The user hasn't previously said not to convert it
const QString versionString = KAlarmCal::getVersionString(compatAttr->version());
const QString msg = conversionPrompt(mCollection.name(), versionString, false);
qCDebug(KALARM_LOG) << "AkonadiCalendarUpdater::update: Version" << versionString;
if (KAMessageBox::warningYesNo(mPromptParent, msg) != KMessageBox::Yes)
result = false; // the user chose not to update the calendar
else
{
// Tell the resource to update the backend storage format
QString errmsg;
if (!mNewCollection)
{
// Refetch the collection's details because anything could
// have happened since the prompt was first displayed.
if (!AkonadiDataModel::instance()->refresh(mCollection))
errmsg = i18nc("@info", "Invalid collection");
}
if (errmsg.isEmpty())
{
const AgentInstance agent = AgentManager::self()->instance(mCollection.resource());
if (mDirResource)
updateStorageFormat<OrgKdeAkonadiKAlarmDirSettingsInterface>(agent, errmsg, mParent);
else
updateStorageFormat<OrgKdeAkonadiKAlarmSettingsInterface>(agent, errmsg, mParent);
}
if (!errmsg.isEmpty())
{
Resources::notifyResourceMessage(mCollection.id(), ResourceType::MessageType::Error,
xi18nc("@info", "Failed to update format of calendar <resource>%1</resource>", mCollection.name()),
errmsg);
}
}
if (!mNewCollection)
{
// Record the user's choice of whether to update the calendar
Resource resource = AkonadiDataModel::instance()->resource(mCollection.id());
resource.setKeepFormat(!result);
}
}
}
}
deleteLater();
return result;
}
/******************************************************************************
* Tell an Akonadi resource to update the backend storage format to the current
* KAlarm format.
* Reply = true if success; if false, 'errorMessage' contains the error message.
*/
template <class Interface> bool AkonadiCalendarUpdater::updateStorageFormat(const AgentInstance& agent, QString& errorMessage, QObject* parent)
{
qCDebug(KALARM_LOG) << "AkonadiCalendarUpdater::updateStorageFormat";
Interface* iface = AkonadiResource::getAgentInterface<Interface>(agent, errorMessage, parent);
if (!iface)
{
qCDebug(KALARM_LOG) << "AkonadiCalendarUpdater::updateStorageFormat:" << errorMessage;
return false;
}
iface->setUpdateStorageFormat(true);
iface->save();
delete iface;
qCDebug(KALARM_LOG) << "AkonadiCalendarUpdater::updateStorageFormat: true";
return true;
}
// vim: et sw=4:
/*
* akonadicalendarupdater.h - updates a calendar to current KAlarm format
* Program: kalarm
* Copyright © 2011-2020 David Jarvie <djarvie@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef AKONADICALENDARUPDATER_H
#define AKONADICALENDARUPDATER_H
#include "calendarupdater.h"
#include <AkonadiCore/Collection>
namespace Akonadi { class AgentInstance; }
// Updates the backend calendar format of a single alarm calendar
class AkonadiCalendarUpdater : public CalendarUpdater
{
Q_OBJECT
public:
AkonadiCalendarUpdater(const Akonadi::Collection& collection, bool dirResource,
bool ignoreKeepFormat, bool newCollection, QObject* parent, QWidget* promptParent = nullptr);
/** If an existing resource calendar can be converted to the current KAlarm
* format, prompt the user whether to convert it, and if yes, tell the resource
* to update the backend storage to the current format.
* The resource's KeepFormat property will be updated if the user chooses not to
* update the calendar.
* This method should call update() on a single shot timer to prompt the
* user and convert the calendar.
* @param parent Parent object. If possible, this should be a QWidget.
*/
static void updateToCurrentFormat(const Resource&, bool ignoreKeepFormat, QObject* parent);
public Q_SLOTS:
/** If the calendar is not in the current KAlarm format, prompt the user
* whether to convert to the current format, and then perform the conversion.
*/
bool update() override;
private:
template <class Interface> static bool updateStorageFormat(const Akonadi::AgentInstance&, QString& errorMessage, QObject* parent);
Akonadi::Collection mCollection;
const bool mDirResource;
const bool mNewCollection;
};
#endif // AKONADICALENDARUPDATER_H
// vim: et sw=4:
......@@ -21,10 +21,10 @@
#include "akonadiresource.h"
#include "resources.h"
#include "akonadicalendarupdater.h"
#include "akonadidatamodel.h"
#include "akonadiresourcemigrator.h"
#include "lib/autoqpointer.h"
#include "kalarm_debug.h"
#include <KAlarmCal/Akonadi>
#include <KAlarmCal/CompatibilityAttribute>
......@@ -47,15 +47,15 @@ using namespace Akonadi;
namespace
{
const QString KALARM_RESOURCE(QStringLiteral("akonadi_kalarm_resource"));
const QString KALARM_DIR_RESOURCE(QStringLiteral("akonadi_kalarm_dir_resource"));
const Collection::Rights WritableRights = Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem;
const QRegularExpression MatchMimeType(QStringLiteral("^application/x-vnd\\.kde\\.alarm.*"),
QRegularExpression::DotMatchesEverythingOption);
}
const QString AkonadiResource::KALARM_RESOURCE(QStringLiteral("akonadi_kalarm_resource"));
const QString AkonadiResource::KALARM_DIR_RESOURCE(QStringLiteral("akonadi_kalarm_dir_resource"));
// Class to provide an object for removeDuplicateResources() signals to be received.
class DuplicateResourceObject : public QObject
{
......@@ -716,7 +716,7 @@ void AkonadiResource::notifyCollectionChanged(Resource& res, const Collection& c
qCDebug(KALARM_LOG) << "AkonadiResource::setCollectionChanged:" << collection.id() << ": compatibility ->" << collection.attribute<CompatibilityAttribute>()->compatibility();
// Note that the AkonadiResource will be deleted once no more
// QSharedPointers reference it.
AkonadiResourceMigrator::updateToCurrentFormat(res, false, akres);
AkonadiCalendarUpdater::updateToCurrentFormat(res, false, akres);
}
}
}
......
/*
* akonadiresource.h - class for an Akonadi alarm calendar resource
* Program: kalarm
* Copyright © 2019 David Jarvie <djarvie@kde.org>
* Copyright © 2019-2020 David Jarvie <djarvie@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
......@@ -23,12 +23,16 @@
#include "resource.h"
#include "kalarm_debug.h"
#include <KAlarmCal/CollectionAttribute>
#include <AkonadiCore/AgentInstance>
#include <AkonadiCore/Collection>
#include <AkonadiCore/Item>
#include <QObject>
#include <QDBusConnection>
class KJob;
class DuplicateResourceObject;
......@@ -306,6 +310,18 @@ public:
*/
static void notifyItemChanged(Resource&, const Akonadi::Item&, bool created);
/** Create a D-Bus interface to an Akonadi resource.
* @return If success: interface;
* If error: null, with 'errorMessage' containing the error message.
*/
template <class Interface>
static Interface* getAgentInterface(const Akonadi::AgentInstance& agent, QString& errorMessage, QObject* parent);
/** The resource type for a single file Akonadi resource. */
static const QString KALARM_RESOURCE;
/** The resource type for a directory Akonadi resource. */
static const QString KALARM_DIR_RESOURCE;
private Q_SLOTS:
void slotCollectionRemoved(const Akonadi::Collection&);
void itemJobDone(KJob*);
......@@ -331,6 +347,30 @@ private:
bool mCollectionAttrChecked{false}; // CollectionAttribute has been processed first time
};
/*=============================================================================
* Template definitions.
*============================================================================*/
/******************************************************************************
* Create a D-Bus interface to an Akonadi resource.
* Reply = interface if success
* = 0 if error: 'errorMessage' contains the error message.
*/
template <class Interface> Interface* AkonadiResource::getAgentInterface(const Akonadi::AgentInstance& agent, QString& errorMessage, QObject* parent)
{
Interface* iface = new Interface(QLatin1String("org.freedesktop.Akonadi.Resource.") + agent.identifier(),
QStringLiteral("/Settings"), QDBusConnection::sessionBus(), parent);
if (!iface->isValid())
{
errorMessage = iface->lastError().message();
qCDebug(KALARM_LOG) << "AkonadiResource::getAgentInterface: D-Bus error accessing resource:" << errorMessage;
delete iface;
return nullptr;
}
return iface;
}
#endif // AKONADIRESOURCE_H
// vim: et sw=4:
/*
* akonadiresourcemigrator.cpp - migrates or creates KAlarm Akonadi resources
* Program: kalarm
* Copyright © 2011-2019 David Jarvie <djarvie@kde.org>
* Copyright © 2011-2020 David Jarvie <djarvie@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
......@@ -25,6 +25,7 @@
#include "mainwindow.h"
#include "resources/akonadidatamodel.h"
#include "resources/akonadiresource.h"
#include "resources/akonadicalendarupdater.h"
#include "resources/resources.h"
#include "lib/messagebox.h"
#include "kalarm_debug.h"
......@@ -51,13 +52,6 @@
using namespace Akonadi;
using namespace KAlarmCal;
namespace
{
const QString KALARM_RESOURCE(QStringLiteral("akonadi_kalarm_resource"));
const QString KALARM_DIR_RESOURCE(QStringLiteral("akonadi_kalarm_dir_resource"));
QString conversionPrompt(const QString& calendarName, const QString& calendarVersion, bool whole);
}
// Creates, or migrates from KResources, a single alarm calendar
class CalendarCreator : public QObject
......@@ -115,36 +109,9 @@ class CalendarCreator : public QObject
bool mFinished{false};
};
// Updates the backend calendar format of a single alarm calendar
class CalendarUpdater : public QObject
{
Q_OBJECT
public:
CalendarUpdater(const Collection& collection, bool dirResource,
bool ignoreKeepFormat, bool newCollection, QObject* parent, QWidget* promptParent = nullptr);
~CalendarUpdater();
// Return whether another instance is already updating this collection
bool isDuplicate() const { return mDuplicate; }
// Check whether any instance is for the given collection ID
static bool containsCollection(Collection::Id);
public Q_SLOTS:
bool update();
private:
static QList<CalendarUpdater*> mInstances;
Akonadi::Collection mCollection;
QObject* mParent;
QWidget* mPromptParent;
const bool mDirResource;
const bool mIgnoreKeepFormat;
const bool mNewCollection;
const bool mDuplicate; // another instance is already updating this collection
};
AkonadiResourceMigrator* AkonadiResourceMigrator::mInstance = nullptr;
bool AkonadiResourceMigrator::mCompleted = false;
bool AkonadiResourceMigrator::mCompleted = false;
AkonadiResourceMigrator::AkonadiResourceMigrator(QObject* parent)
: QObject(parent)
......@@ -197,7 +164,7 @@ void AkonadiResourceMigrator::migrateOrCreate()
for (const AgentInstance& agent : agents)
{
const QString type = agent.type().identifier();
if (type == KALARM_RESOURCE || type == KALARM_DIR_RESOURCE)
if (type == AkonadiResource::KALARM_RESOURCE || type == AkonadiResource::KALARM_DIR_RESOURCE)
{
// Fetch the resource's collection to determine its alarm types
CollectionFetchJob* job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel);
......@@ -229,11 +196,11 @@ void AkonadiResourceMigrator::migrateOrCreate()
const QString resourceType = configGroup.readEntry("ResourceType", QString());
QString agentType;
if (resourceType == QLatin1String("file"))
agentType = KALARM_RESOURCE;
agentType = AkonadiResource::KALARM_RESOURCE;
else if (resourceType == QLatin1String("dir"))
agentType = KALARM_DIR_RESOURCE;
agentType = AkonadiResource::KALARM_DIR_RESOURCE;
else if (resourceType == QLatin1String("remote"))
agentType = KALARM_RESOURCE;
agentType = AkonadiResource::KALARM_RESOURCE;
else
continue; // unknown resource type - can't convert
......@@ -301,7 +268,7 @@ void AkonadiResourceMigrator::createDefaultResources()
connect(creator, &CalendarCreator::finished, this, &AkonadiResourceMigrator::calendarCreated);
connect(creator, &CalendarCreator::creating, this, &AkonadiResourceMigrator::creatingCalendar);
mCalendarsPending << creator;
creator->createAgent(KALARM_RESOURCE, this);
creator->createAgent(AkonadiResource::KALARM_RESOURCE, this);
}
if (!(mExistingAlarmTypes & CalEvent::ARCHIVED))
{
......@@ -309,7 +276,7 @@ void AkonadiResourceMigrator::createDefaultResources()
connect(creator, &CalendarCreator::finished, this, &AkonadiResourceMigrator::calendarCreated);
connect(creator, &CalendarCreator::creating, this, &AkonadiResourceMigrator::creatingCalendar);
mCalendarsPending << creator;
creator->createAgent(KALARM_RESOURCE, this);
creator->createAgent(AkonadiResource::KALARM_RESOURCE, this);
}
if (!(mExistingAlarmTypes & CalEvent::TEMPLATE))
{
......@@ -317,7 +284,7 @@ void AkonadiResourceMigrator::createDefaultResources()
connect(creator, &CalendarCreator::finished, this, &AkonadiResourceMigrator::calendarCreated);
connect(creator, &CalendarCreator::creating, this, &AkonadiResourceMigrator::creatingCalendar);
mCalendarsPending << creator;
creator->createAgent(KALARM_RESOURCE, this);
creator->createAgent(AkonadiResource::KALARM_RESOURCE, this);
}
if (mCalendarsPending.isEmpty())
......@@ -369,178 +336,6 @@ void AkonadiResourceMigrator::calendarCreated(CalendarCreator* creator)
}
}
/******************************************************************************
* If an existing Akonadi resource calendar can be converted to the current
* KAlarm format, prompt the user whether to convert it, and if yes, tell the
* Akonadi resource to update the backend storage to the current format.
* The CollectionAttribute's KeepFormat property will be updated if the user
* chooses not to update the calendar.
*
* Note: the collection should be up to date: use AkonadiDataModel::refresh()
* before calling this function.
*/
void AkonadiResourceMigrator::updateToCurrentFormat(const Resource& resource, bool ignoreKeepFormat, QObject* parent)
{
qCDebug(KALARM_LOG) << "AkonadiResourceMigrator::updateToCurrentFormat:" << resource.id();
if (CalendarUpdater::containsCollection(resource.id()))
return; // prevent multiple simultaneous user prompts
const AgentInstance agent = AgentManager::self()->instance(resource.configName());
const QString id = agent.type().identifier();
bool dirResource;
if (id == KALARM_RESOURCE)
dirResource = false;
else if (id == KALARM_DIR_RESOURCE)
dirResource = true;
else
{
qCCritical(KALARM_LOG) << "AkonadiResourceMigrator::updateToCurrentFormat: Invalid agent type" << id;
return;
}
const Collection& collection = AkonadiResource::collection(resource);
CalendarUpdater* updater = new CalendarUpdater(collection, dirResource, ignoreKeepFormat, false, parent, qobject_cast<QWidget*>(parent));
QTimer::singleShot(0, updater, &CalendarUpdater::update);
}
/*===========================================================================*/
QList<CalendarUpdater*> CalendarUpdater::mInstances;
CalendarUpdater::CalendarUpdater(const Collection& collection, bool dirResource,
bool ignoreKeepFormat, bool newCollection, QObject* parent, QWidget* promptParent)
: QObject(parent)
, mCollection(collection)
, mParent(parent)
, mPromptParent(promptParent ? promptParent : MainWindow::mainMainWindow())
, mDirResource(dirResource)
, mIgnoreKeepFormat(ignoreKeepFormat)
, mNewCollection(newCollection)
, mDuplicate(containsCollection(collection.id()))
{
mInstances.append(this);
}
CalendarUpdater::~CalendarUpdater()
{
mInstances.removeAll(this);
}
bool CalendarUpdater::containsCollection(Collection::Id id)
{
for (CalendarUpdater* instance : mInstances)
{
if (instance->mCollection.id() == id)
return true;
}
return false;
}
bool CalendarUpdater::update()
{
qCDebug(KALARM_LOG) << "CalendarUpdater::update:" << mCollection.id() << (mDirResource ? "directory" : "file");
bool result = true;
if (mDuplicate)
qCDebug(KALARM_LOG) << "CalendarUpdater::update: Not updating (concurrent update in progress)";
else if (mCollection.hasAttribute<CompatibilityAttribute>()) // must know format to update
{
const CompatibilityAttribute* compatAttr = mCollection.attribute<CompatibilityAttribute>();
const KACalendar::Compat compatibility = compatAttr->compatibility();
qCDebug(KALARM_LOG) << "CalendarUpdater::update: current format:" << compatibility;
if ((compatibility & ~KACalendar::Converted)
// The calendar isn't in the current KAlarm format
&& !(compatibility & ~(KACalendar::Convertible | KACalendar::Converted)))
{
// The calendar format is convertible to the current KAlarm format
if (!mIgnoreKeepFormat
&& mCollection.hasAttribute<CollectionAttribute>()
&& mCollection.attribute<CollectionAttribute>()->keepFormat())
qCDebug(KALARM_LOG) << "CalendarUpdater::update: Not updating format (previous user choice)";
else
{
// The user hasn't previously said not to convert it
const QString versionString = KAlarmCal::getVersionString(compatAttr->version());
const QString msg = conversionPrompt(mCollection.name(), versionString, false);
qCDebug(KALARM_LOG) << "CalendarUpdater::update: Version" << versionString;
if (KAMessageBox::warningYesNo(mPromptParent, msg) != KMessageBox::Yes)
result = false; // the user chose not to update the calendar
else
{
// Tell the resource to update the backend storage format
QString errmsg;
if (!mNewCollection)
{
// Refetch the collection's details because anything could
// have happened since the prompt was first displayed.
if (!AkonadiDataModel::instance()->refresh(mCollection))
errmsg = i18nc("@info", "Invalid collection");
}
if (errmsg.isEmpty())
{
const AgentInstance agent = AgentManager::self()->instance(mCollection.resource());
if (mDirResource)
AkonadiResourceMigrator::updateStorageFormat<OrgKdeAkonadiKAlarmDirSettingsInterface>(agent, errmsg, mParent);
else
AkonadiResourceMigrator::updateStorageFormat<OrgKdeAkonadiKAlarmSettingsInterface>(agent, errmsg, mParent);
}
if (!errmsg.isEmpty())
{
Resources::notifyResourceMessage(mCollection.id(), ResourceType::MessageType::Error,
xi18nc("@info", "Failed to update format of calendar <resource>%1</resource>", mCollection.name()),