Development/Tutorials/Akonadi/Resources: Difference between revisions
m (Wiki Link instead of http URL) |
(Mark for updating) |
||
(43 intermediate revisions by 11 users not shown) | |||
Line 1: | Line 1: | ||
{{TutorialBrowser| | {{TutorialBrowser| | ||
series=Akonadi Tutorial| | series=[[Development/Tutorials#Personal_Information_Management_.28Akonadi.29|Akonadi Tutorial]]| | ||
name=Creating an Akonadi PIM data Resource| | name=Creating an Akonadi PIM data Resource| | ||
pre=[http://mindview.net/Books/TICPP/ThinkingInCPP2e.html C++], [http://www.trolltech.com/products/qt/ Qt], [[Getting_Started/Build | pre=[http://mindview.net/Books/TICPP/ThinkingInCPP2e.html C++], [http://www.trolltech.com/products/qt/ Qt], [[Getting_Started/Build|KDE development environment]]| | ||
next=| | next=| | ||
reading=[[Development/Tutorials/CMake|CMake]] | reading=[[Development/Tutorials/CMake|CMake]], [[Development/Architecture/KDE4/Akonadi|Akonadi Architecture]], [[Projects/PIM/Akonadi/Development_Tools|Akonadi Development Tools]] | ||
}} | }} | ||
{{Review|Port to KF5}} | |||
This tutorial will guide you through the steps of creating an Akonadi resource, an agent program which transports PIM data between the Akonadi system and a storage backend. | This tutorial will guide you through the steps of creating an Akonadi resource, an agent program which transports PIM data between the Akonadi system and a storage backend. | ||
See the [http://api.kde.org/4.x-api/kdepimlibs-apidocs/akonadi/html/classAkonadi_1_1ResourceBase.html#_details Resource related API documentation] for developer information not covered in this tutorial. | |||
The resource developed in this tutorial will use a directory on the local file system as its backend and handle contact data, i.e. address book entries. | The resource developed in this tutorial will use a directory on the local file system as its backend and handle contact data, i.e. address book entries. | ||
For other real-life examples of akonadi resources see projects in [https://projects.kde.org/projects/kde/kdepim-runtime/repository/revisions/master/show/resources /kdepim-runtime/resources] KDE source directory. | |||
{{improve|Add error handling to resource code snippets}} | {{improve|Add error handling to resource code snippets}} | ||
Line 24: | Line 32: | ||
== Preparation == | == Preparation == | ||
We can kick-start the resource by using '''KAppTemplate''', which can be found as '''KDE template generator''' in the development section of the K-menu. | The KDE client library for Akonadi provides a base class, [http://api.kde.org/4.x-api/kdepimlibs-apidocs/akonadi/html/classAkonadi_1_1ResourceBase.html Akonadi::ResourceBase], which already implements most of the low level communication between resource and Akonadi server, letting the resource developer concentrate on communication with the backend. | ||
We can kick-start the resource by using '''KAppTemplate''', which can be found as '''KDE template generator''' in the development section of the K-menu, or by running '''kapptemplate''' in a terminal window. | |||
First, we select the '''Akonadi Resource Template''' in the C++ section of the program, give our project a name and continue through the following pages to complete the template creation. | First, we select the '''Akonadi Resource Template''' in the C++ section of the program, give our project a name and continue through the following pages to complete the template creation. | ||
Line 31: | Line 41: | ||
A look at the generated project directory shows us the following files: | A look at the generated project directory shows us the following files: | ||
< | <syntaxhighlight lang="bash"> | ||
akonadi-resources.png | akonadi-resources.png | ||
Messages.sh | Messages.sh | ||
Line 41: | Line 51: | ||
vcarddirresource.cpp | vcarddirresource.cpp | ||
vcarddirresource.h | vcarddirresource.h | ||
</ | </syntaxhighlight> | ||
At this stage it is already possible to compile the resource, so we can already check if our development environment is setup correctly by creating the build directory and having CMake either generate Makefiles or a KDevelop project file. | At this stage it is already possible to compile the resource, so we can already check if our development environment is setup correctly by creating the build directory and having CMake either generate Makefiles or a KDevelop project file. | ||
Line 47: | Line 57: | ||
=== Generating Makefiles === | === Generating Makefiles === | ||
< | From within the generated source directory: | ||
<syntaxhighlight lang="bash"> | |||
mkdir build | mkdir build | ||
cd build | cd build | ||
cmake -DCMAKE_BUILD_TYPE=debugfull .. | cmake -DCMAKE_BUILD_TYPE=debugfull .. | ||
</ | </syntaxhighlight> | ||
and run the build using make as usual. | and run the build using make as usual. | ||
Note: if after running cmake you receive error like: "Could NOT find KdepimLibs (missing: KdepimLibs_CONFIG)", try running cmake with additional parameter: | |||
<syntaxhighlight lang="bash"> | |||
cmake -DCMAKE_BUILD_TYPE=debugfull -DKdepimLibs_DIR=/usr/lib64/cmake/KdepimLibs .. | |||
</syntaxhighlight> | |||
=== Generating a KDevelop project file === | === Generating a KDevelop project file === | ||
< | From within the generated source directory: | ||
<syntaxhighlight lang="bash"> | |||
mkdir build | mkdir build | ||
cd build | cd build | ||
cmake -DCMAKE_BUILD_TYPE=debugfull -G KDevelop3 .. | cmake -DCMAKE_BUILD_TYPE=debugfull -G KDevelop3 .. | ||
</ | </syntaxhighlight> | ||
and open the generated project with KDevelop and run the build process from there. | and open the generated project with KDevelop and run the build process from there. | ||
Line 70: | Line 87: | ||
Since the '''vcarddirresource.desktop''' file generated by KAppTemplate contains only example values, we need to edit it: | Since the '''vcarddirresource.desktop''' file generated by KAppTemplate contains only example values, we need to edit it: | ||
< | <syntaxhighlight lang="ini"> | ||
[Desktop Entry] | [Desktop Entry] | ||
Name=Akonadi VCardDir Resource | Name=Akonadi VCardDir Resource | ||
Line 82: | Line 99: | ||
X-Akonadi-Capabilities=Resource | X-Akonadi-Capabilities=Resource | ||
X-Akonadi-Identifier=akonadi_vcarddir_resource | X-Akonadi-Identifier=akonadi_vcarddir_resource | ||
</ | </syntaxhighlight> | ||
'''Name''' and '''Comment''' are strings visible to the user and can be translated. Since our resource will serve contact data, the example valued for '''Icon''' and '''X-Akonadi-MimeTypes''' fields are conveniently correct. | '''Name''' and '''Comment''' are strings visible to the user and can be translated. Since our resource will serve contact data, the example valued for '''Icon''' and '''X-Akonadi-MimeTypes''' fields are conveniently correct. | ||
Line 92: | Line 109: | ||
KDE has a nice framework for this called [[Development/Tutorials/Using_KConfig_XT| KConfig XT]] which enabled us developers to specify our config options as an XML file and have the code for storage and retrieval auto-generated. | KDE has a nice framework for this called [[Development/Tutorials/Using_KConfig_XT| KConfig XT]] which enabled us developers to specify our config options as an XML file and have the code for storage and retrieval auto-generated. | ||
The template we are basing our resource on already contains such a file, '''vcarddirresource.kcfg''', and we only have to add | The template we are basing our resource on already contains such a file, '''vcarddirresource.kcfg''', and we only have to add a new option for our base directory: | ||
< | <syntaxhighlight lang="xml"> | ||
<?xml version="1.0" encoding="UTF-8"?> | <?xml version="1.0" encoding="UTF-8"?> | ||
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0" | <kcfg xmlns="http://www.kde.org/standards/kcfg/1.0" | ||
Line 111: | Line 128: | ||
</group> | </group> | ||
</kcfg> | </kcfg> | ||
</ | </syntaxhighlight> | ||
To enable the user to change this properties, we need to create and show a config dialog. Since we are using KConfig XT this is pretty easy, but for the scope of this tutorial we make our lives even | To enable the user to change this properties, we need to create and show a config dialog. Since we are using KConfig XT this is pretty easy, but for the scope of this tutorial we make our lives even easier and just use a {{class|KFileDialog}} | ||
{{warning|This simplification has the side effect that the passed window ID cannot be used as it should. See FAQ section at the end of the tutorial.}} | |||
First we need to add two new include directives at the beginning of our source file '''vcarddirresource.cpp''': | First we need to add two new include directives at the beginning of our source file '''vcarddirresource.cpp''': | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
#include <kfiledialog.h> | #include <kfiledialog.h> | ||
#include <klocalizedstring.h> | #include <klocalizedstring.h> | ||
</ | </syntaxhighlight> | ||
To show the dialog on the user's request, we need to modify the resource method <tt>configure</tt> which currently has an empty | To show the dialog on the user's request, we need to modify the resource method <tt>configure</tt> which currently has an empty implementation. | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
void VCardDirResource::configure( WId windowId ) | void VCardDirResource::configure( WId windowId ) | ||
{ | { | ||
Line 149: | Line 168: | ||
synchronize(); | synchronize(); | ||
} | } | ||
</ | </syntaxhighlight> | ||
The call to <tt>synchronize</tt> at the end tells Akonadi to start retrieving the new data from our resource. | The call to <tt>synchronize</tt> at the end tells Akonadi to start retrieving the new data from our resource. | ||
{{tip|Since there can be more than one resource of a certain type, it is recommended to change the resource name to something that makes them distiguishable. In the case of this example resource it could be the name of the base directory or part of its path}} | |||
== Data Retrieval == | == Data Retrieval == | ||
Line 165: | Line 186: | ||
Retrieval of collections is handled in <tt>retrieveCollections</tt>: | Retrieval of collections is handled in <tt>retrieveCollections</tt>: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
void VCardDirResource::retrieveCollections() | void VCardDirResource::retrieveCollections() | ||
{ | { | ||
Line 174: | Line 195: | ||
QStringList mimeTypes; | QStringList mimeTypes; | ||
mimeTypes << "text/directory"; | mimeTypes << QLatin1String("text/directory"); | ||
c.setContentMimeTypes( mimeTypes ); | c.setContentMimeTypes( mimeTypes ); | ||
Line 181: | Line 202: | ||
collectionsRetrieved( list ); | collectionsRetrieved( list ); | ||
} | } | ||
</ | </syntaxhighlight> | ||
The code creates a top level collection, i.e. its parent collection is Akonadi's root collection, sets the configured path as its remote identifier (so we could eventually map it back to our "backend" location) and uses our resource name as the user visible collection name. | The code creates a top level collection, i.e. its parent collection is Akonadi's root collection, sets the configured path as its remote identifier (so we could eventually map it back to our "backend" location) and uses our resource name as the user visible collection name. | ||
Line 187: | Line 208: | ||
Since our resource will be providing contact data, we need to set the respective MIME type to indicate which kind of data our collection will hold. | Since our resource will be providing contact data, we need to set the respective MIME type to indicate which kind of data our collection will hold. | ||
Finally the fully | Finally the fully set up collection is sent to Akonadi. | ||
{{tip|You can customize visualization properties of the collection, e.g. icon, by using the [http://api.kde.org/4.x-api/kdepimlibs-apidocs/akonadi/html/classAkonadi_1_1EntityDisplayAttribute.html EntityDisplayAttribute] class}} | |||
=== Retrieving Items === | === Retrieving Items === | ||
Line 199: | Line 222: | ||
First we need another include directive: | First we need another include directive: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
#include <QtCore/QDir> | #include <QtCore/QDir> | ||
</ | </syntaxhighlight> | ||
Then we implement <tt>retrieveItems</tt>: | Then we implement <tt>retrieveItems</tt>: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
void VCardDirResource::retrieveItems( const Akonadi::Collection &collection ) | void VCardDirResource::retrieveItems( const Akonadi::Collection &collection ) | ||
{ | { | ||
Line 226: | Line 249: | ||
itemsRetrieved( items ); | itemsRetrieved( items ); | ||
} | } | ||
</ | </syntaxhighlight> | ||
Our earlier decision to use the directory's path as the collection's remote identifier conveniently allows us to retrieve the path from the given collection. | Our earlier decision to use the directory's path as the collection's remote identifier conveniently allows us to retrieve the path from the given collection. | ||
Line 246: | Line 269: | ||
First we need another set of includes: | First we need another set of includes: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
#include <kabc/addressee.h> | #include <kabc/addressee.h> | ||
#include <kabc/vcardconverter.h> | #include <kabc/vcardconverter.h> | ||
</ | </syntaxhighlight> | ||
Then we implement the <tt>retrieveItem</tt> method: | Then we implement the <tt>retrieveItem</tt> method: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
bool VCardDirResource::retrieveItem( const Akonadi::Item &item, const QSet<QByteArray> &parts ) | bool VCardDirResource::retrieveItem( const Akonadi::Item &item, const QSet<QByteArray> &parts ) | ||
{ | { | ||
Line 278: | Line 301: | ||
return true; | return true; | ||
} | } | ||
</ | </syntaxhighlight> | ||
And additionally we need to edit '''CMakeLists.txt''' to make it link the library for the <tt>KABC</tt> classes: | And additionally we need to edit '''CMakeLists.txt''' to make it link the library for the <tt>KABC</tt> classes: | ||
< | <syntaxhighlight lang="cmake"> | ||
target_link_libraries(akonadi_vcarddir_resource ${KDE4_AKONADI_LIBS} ${QT_QTCORE_LIBRARY} ${QT_QTDBUS_LIBRARY} ${KDE4_KDECORE_LIBS} ${KDE4_KABC_LIBS}) | target_link_libraries(akonadi_vcarddir_resource ${KDE4_AKONADI_LIBS} ${QT_QTCORE_LIBRARY} ${QT_QTDBUS_LIBRARY} ${KDE4_KDECORE_LIBS} ${KDE4_KABC_LIBS}) | ||
</ | </syntaxhighlight> | ||
Again aided by our decision what to use as the item's remote identifier, we can directly use it to open the respective vCard file. | Again aided by our decision what to use as the item's remote identifier, we can directly use it to open the respective vCard file. | ||
Line 293: | Line 316: | ||
All Akonadi clients, e.g. end user applications, agents, etc., can at any time add items to collections, change item data and remove items from collections. If such a change happens in the collection tree of a resource, the resource should properly make the respective change on its backend. | All Akonadi clients, e.g. end user applications, agents, etc., can at any time add items to collections, change item data and remove items from collections. If such a change happens in the collection tree of a resource, the resource should properly make the respective change on its backend. | ||
By default, resources are only notified about which items have been changed and which of the parts have been modified. | |||
To automatically the get the updated payload we only have to enable this operation mode on the resource's change recorder. | |||
To do that we need another two include directives | |||
<syntaxhighlight lang="cpp-qt"> | |||
#include <akonadi/itemfetchscope.h> | |||
#include <akonadi/changerecorder.h> | |||
</syntaxhighlight> | |||
and an additional line in the resource's constructor | |||
<syntaxhighlight lang="cpp-qt"> | |||
changeRecorder()->itemFetchScope().fetchFullPayload(); | |||
</syntaxhighlight> | |||
This tells the base implementation that each item change should be fetched with full payload before delivering it to the methods covered in the following subsections. | |||
{{tip|'''Exercise:''' Add handling for collection changes, e.g. create subdirectories when child collections have been created.}} | {{tip|'''Exercise:''' Add handling for collection changes, e.g. create subdirectories when child collections have been created.}} | ||
Line 298: | Line 339: | ||
=== Adding Items === | === Adding Items === | ||
In order to handle items being added to one of our collections, we need to implement the method <tt>itemAdded</tt>. | |||
First we need another include directive for a helper class: | First we need another include directive for a helper class: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
#include <krandom.h> | #include <krandom.h> | ||
</ | </syntaxhighlight> | ||
Now we can implement <tt>itemAdded</tt> like this: | Now we can implement <tt>itemAdded</tt> like this: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
void VCardDirResource::itemAdded( const Akonadi::Item &item, const Akonadi::Collection &collection ) | void VCardDirResource::itemAdded( const Akonadi::Item &item, const Akonadi::Collection &collection ) | ||
{ | { | ||
Line 334: | Line 375: | ||
changeCommitted( newItem ); | changeCommitted( newItem ); | ||
} | } | ||
</ | </syntaxhighlight> | ||
As usual we get the path of the directory a specific collection maps to from the collection's remote identifier. | As usual we get the path of the directory a specific collection maps to from the collection's remote identifier. | ||
Line 347: | Line 388: | ||
In our case handling item modifications is almost the same as handling item adding, however more sophisticated resource will most likely do different things depending on the <tt>parts</tt> parameter. | In our case handling item modifications is almost the same as handling item adding, however more sophisticated resource will most likely do different things depending on the <tt>parts</tt> parameter. | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
void VCardDirResource::itemChanged( const Akonadi::Item &item, const QSet<QByteArray> &parts ) | void VCardDirResource::itemChanged( const Akonadi::Item &item, const QSet<QByteArray> &parts ) | ||
{ | { | ||
Line 376: | Line 417: | ||
changeCommitted( newItem ); | changeCommitted( newItem ); | ||
} | } | ||
</ | </syntaxhighlight> | ||
=== Removing Items === | === Removing Items === | ||
Line 382: | Line 423: | ||
Since we have a one-to-one mapping of items to files, an item removal is as simple as removing the respective file: | Since we have a one-to-one mapping of items to files, an item removal is as simple as removing the respective file: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
void VCardDirResource::itemRemoved( const Akonadi::Item &item ) | void VCardDirResource::itemRemoved( const Akonadi::Item &item ) | ||
{ | { | ||
Line 393: | Line 434: | ||
changeCommitted( item ); | changeCommitted( item ); | ||
} | } | ||
</ | </syntaxhighlight> | ||
== Backend Changes == | == Backend Changes == | ||
Line 405: | Line 446: | ||
== Testing == | == Testing == | ||
{{improve| | The [[Projects/PIM/Akonadi/Testing|Akonadi Test Framework]] allows to create self-contained test environments so developers do not risk impacting their normal Akonadi setup. | ||
To install new resource to system run (might need root): | |||
<syntaxhighlight lang="bash"> | |||
make install | |||
</syntaxhighlight> | |||
or copy built files manually to the following locations: | |||
<syntaxhighlight lang="bash"> | |||
$KDEDIRS/bin/akonadi_vcarddir_resource | |||
$KDEDIRS/share/akonadi/agents/vcarddirresource.desktop | |||
</syntaxhighlight> | |||
where $KDEDIRS points to your KDE installation prefix. | |||
Then run Akonadi Configuration (search by this keyword in kickoff launcher) and click "Add..." button to add new resource - new resource should be there; or in KAddressBook add new address book - new resource should be available as address book type (restarting akonadi server might help if it does not get into this list immediately). | |||
{{improve|Create tutorial about using the test suite.}} | |||
== Frequently Asked Questions and Tips == | |||
=== Base Class Methods === | |||
==== When is configure() called ==== | |||
[http://api.kde.org/4.x-api/kdepimlibs-apidocs/akonadi/html/classAkonadi_1_1AgentBase.html#88a08911cd0a69934207ef1a154a23ba ResourceBase::configure()] is called under two circumstances. | |||
* as part of the resource's creation process: when the Akonadi control process creates a resource, it will also call the resource's '''configure()''' method through D-Bus once the resource has registered itself. | |||
* when another program explicitly requests it: since '''configure()''' is exposed as a D-Bus method, any other program can call it to request a resource's reconfiguration. End user applications might offer this functionality to allow the user to change resource specific settings. | |||
==== What is the purpose of the window ID parameter of configure()? ==== | |||
The resource is a program on its own, i.e. it is not the same process as the end user application working with the data provided by the resource. | |||
In an in-process scenario any configuration UI can specify one of the application's top level widgets as its parent, thus associating the configuration UI with that window. | |||
Window managing systems such as KDE's KWin use this information to keep new windows from interrupting the user unless they are very likely a consequence of a user's action. | |||
To make such an association across process boundaries, the calling application can send a platforms specific window identifier over to the resource which in tunr can then use it to specify it as a "parent" for its configuration UI like this: | |||
<syntaxhighlight lang="cpp-qt"> | |||
ConfigDialog dialog; | |||
KWindowSystem::setMainWindow( &dialog, windowId ); | |||
</syntaxhighlight> | |||
See {{class|KWindowSystem}} | |||
==== Is retrieveItem() called in the order retrieveItems() passes the items? ==== | |||
The purpose of [http://api.kde.org/4.x-api/kdepimlibs-apidocs/akonadi/html/classAkonadi_1_1ResourceBase.html#38d7c3713ed54dedf885d437de1ca11d ResourceBase::retrieveItem()] is to deliver additional item data, i.e. data not yet available in the Akonadi cache. | |||
This can happen due to several different reasons, only one of them is '''retrieveItems()''' delivering just basic items. | |||
An implementation should therefore '''always''' treat an invocation of this method as a single incidence unrelated to anything else. | |||
[[Category:Tutorial]] | [[Category:Tutorial]] | ||
Line 411: | Line 505: | ||
[[Category:KDE4]] | [[Category:KDE4]] | ||
[[Category:Akonadi]] | [[Category:Akonadi]] | ||
[[Category:PIM]] |
Latest revision as of 12:06, 31 May 2019
Tutorial Series | Akonadi Tutorial |
Previous | C++, Qt, KDE development environment |
What's Next | |
Further Reading | CMake, Akonadi Architecture, Akonadi Development Tools |
Parts to be reviewed:
Port to KF5This tutorial will guide you through the steps of creating an Akonadi resource, an agent program which transports PIM data between the Akonadi system and a storage backend.
See the Resource related API documentation for developer information not covered in this tutorial.
The resource developed in this tutorial will use a directory on the local file system as its backend and handle contact data, i.e. address book entries.
For other real-life examples of akonadi resources see projects in /kdepim-runtime/resources KDE source directory.
cleanup confusing sections and fix sections which contain a todo
Add error handling to resource code snippets
Prerequisites
cleanup confusing sections and fix sections which contain a todo
Describe required versions and build setup
Preparation
The KDE client library for Akonadi provides a base class, Akonadi::ResourceBase, which already implements most of the low level communication between resource and Akonadi server, letting the resource developer concentrate on communication with the backend.
We can kick-start the resource by using KAppTemplate, which can be found as KDE template generator in the development section of the K-menu, or by running kapptemplate in a terminal window.
First, we select the Akonadi Resource Template in the C++ section of the program, give our project a name and continue through the following pages to complete the template creation.
A look at the generated project directory shows us the following files:
akonadi-resources.png
Messages.sh
settings.kcfgc
vcarddirresource.desktop
vcarddirresource.kcfg
CMakeLists.txt
README
vcarddirresource.cpp
vcarddirresource.h
At this stage it is already possible to compile the resource, so we can already check if our development environment is setup correctly by creating the build directory and having CMake either generate Makefiles or a KDevelop project file.
Generating Makefiles
From within the generated source directory:
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=debugfull ..
and run the build using make as usual.
Note: if after running cmake you receive error like: "Could NOT find KdepimLibs (missing: KdepimLibs_CONFIG)", try running cmake with additional parameter:
cmake -DCMAKE_BUILD_TYPE=debugfull -DKdepimLibs_DIR=/usr/lib64/cmake/KdepimLibs ..
Generating a KDevelop project file
From within the generated source directory:
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=debugfull -G KDevelop3 ..
and open the generated project with KDevelop and run the build process from there.
Adjusting the resource description file
The capabilities of the resource need to be described in both human understable and machine interpretable form. This is achieved through a so-called desktop file, similar to those installed by applications.
Since the vcarddirresource.desktop file generated by KAppTemplate contains only example values, we need to edit it:
[Desktop Entry]
Name=Akonadi VCardDir Resource
Comment=Resource for a directory containing contact data files
Type=AkonadiResource
Exec=akonadi_vcarddir_resource
Icon=text-directory
X-Akonadi-MimeTypes=text/directory
X-Akonadi-Capabilities=Resource
X-Akonadi-Identifier=akonadi_vcarddir_resource
Name and Comment are strings visible to the user and can be translated. Since our resource will serve contact data, the example valued for Icon and X-Akonadi-MimeTypes fields are conveniently correct.
Resource Configuration
Since the backend of our resource will be a file system directory, we need a way to let the user configure it and a way to persistantly store this configuration.
KDE has a nice framework for this called KConfig XT which enabled us developers to specify our config options as an XML file and have the code for storage and retrieval auto-generated.
The template we are basing our resource on already contains such a file, vcarddirresource.kcfg, and we only have to add a new option for our base directory:
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<group name="General">
<entry name="Path" type="Path">
<label>Path to a folder containing vCard files.</label>
<default></default>
</entry>
<entry name="ReadOnly" type="Bool">
<label>Do not change the actual backend data.</label>
<default>false</default>
</entry>
</group>
</kcfg>
To enable the user to change this properties, we need to create and show a config dialog. Since we are using KConfig XT this is pretty easy, but for the scope of this tutorial we make our lives even easier and just use a KFileDialog
First we need to add two new include directives at the beginning of our source file vcarddirresource.cpp:
#include <kfiledialog.h>
#include <klocalizedstring.h>
To show the dialog on the user's request, we need to modify the resource method configure which currently has an empty implementation.
void VCardDirResource::configure( WId windowId )
{
Q_UNUSED( windowId );
const QString oldPath = Settings::self()->path();
KUrl url;
if ( !oldPath.isEmpty() )
url = KUrl::fromPath( oldPath );
else
url = KUrl::fromPath( QDir::homePath() );
const QString title = i18nc( "@title:window", "Select vCard folder" );
const QString newPath = KFileDialog::getExistingDirectory( url, 0, title );
if ( newPath.isEmpty() )
return;
if ( oldPath == newPath )
return;
Settings::self()->setPath( newPath );
Settings::self()->writeConfig();
synchronize();
}
The call to synchronize at the end tells Akonadi to start retrieving the new data from our resource.
Data Retrieval
In this section of the tutorial we will implement the data retrieval methods of our resource, i.e. the method which Akonadi will call to get the contact data our resource is providing.
Retrieving Collections
The first thing Akonadi will ask for are our collections. Resources can organize their data in a tree of collections, similar to how files are organized in a tree of directories on the file system.
For now we only want to support one directory so we only need one collection.
Retrieval of collections is handled in retrieveCollections:
void VCardDirResource::retrieveCollections()
{
Collection c;
c.setParent( Collection::root() );
c.setRemoteId( Settings::self()->path() );
c.setName( name() );
QStringList mimeTypes;
mimeTypes << QLatin1String("text/directory");
c.setContentMimeTypes( mimeTypes );
Collection::List list;
list << c;
collectionsRetrieved( list );
}
The code creates a top level collection, i.e. its parent collection is Akonadi's root collection, sets the configured path as its remote identifier (so we could eventually map it back to our "backend" location) and uses our resource name as the user visible collection name.
Since our resource will be providing contact data, we need to set the respective MIME type to indicate which kind of data our collection will hold.
Finally the fully set up collection is sent to Akonadi.
Retrieving Items
Retrieving items for a specific collection is similar to listing the files of a specific directory. Conveniently our backend is a directory, so we can map this directly.
There are several methods to implement item retrieval, e.g. synchronous or asynchronous, all items in one go or in patches, etc. For the scope of the tutorial we choose the easiest combination, i.e. synchronously provide all items in one go.
First we need another include directive:
#include <QtCore/QDir>
Then we implement retrieveItems:
void VCardDirResource::retrieveItems( const Akonadi::Collection &collection )
{
// our collections have the mapped path as their remote identifier
const QString path = collection.remoteId();
QDir dir( path );
QStringList filters;
filters << QLatin1String( "*.vcf" );
const QStringList fileList = dir.entryList( filters, QDir::Files );
Item::List items;
foreach( const QString &file, fileList ) {
Item item( QLatin1String( "text/directory" ) );
item.setRemoteId( path + QLatin1Char( '/' ) + file );
items << item;
}
itemsRetrieved( items );
}
Our earlier decision to use the directory's path as the collection's remote identifier conveniently allows us to retrieve the path from the given collection.
We then list all vCard files in that directory and create one Akonadi item for each one, this time using the path plus the file's name as the remote identifier.
Finally the whole list of items is sent to Akonadi.
Retrieving Item Data
Continuing our file system analogy, retrieving a specific item's data is similar to reading a specific file's data or its associated meta data. So we have again the convenience of a direct mapping between Akonadi and backend functionality.
Since our backend items are vCard files, we need to parse them into data structures which we can then use as item payloads.
KDE's class for holding contact data is KABC::Addressee and we can use KABC::VCardConverter for file parsing.
First we need another set of includes:
#include <kabc/addressee.h>
#include <kabc/vcardconverter.h>
Then we implement the retrieveItem method:
bool VCardDirResource::retrieveItem( const Akonadi::Item &item, const QSet<QByteArray> &parts )
{
Q_UNUSED( parts );
const QString fileName = item.remoteId();
QFile file( fileName );
if ( !file.open( QFile::ReadOnly ) )
return false;
const QByteArray data = file.readAll();
if ( file.error() != QFile::NoError )
return false;
KABC::VCardConverter converter;
KABC::Addressee addressee = converter.parseVCard( data );
if ( addressee.isEmpty() )
return false;
Item newItem( item );
newItem.setPayload<KABC::Addressee>( addressee );
itemRetrieved( newItem );
return true;
}
And additionally we need to edit CMakeLists.txt to make it link the library for the KABC classes:
target_link_libraries(akonadi_vcarddir_resource ${KDE4_AKONADI_LIBS} ${QT_QTCORE_LIBRARY} ${QT_QTDBUS_LIBRARY} ${KDE4_KDECORE_LIBS} ${KDE4_KABC_LIBS})
Again aided by our decision what to use as the item's remote identifier, we can directly use it to open the respective vCard file. After parsing the data we create a new item object, use the given item to initialize it, set the contact data object as its payload and send it to Akonadi.
Item Changes
All Akonadi clients, e.g. end user applications, agents, etc., can at any time add items to collections, change item data and remove items from collections. If such a change happens in the collection tree of a resource, the resource should properly make the respective change on its backend.
By default, resources are only notified about which items have been changed and which of the parts have been modified. To automatically the get the updated payload we only have to enable this operation mode on the resource's change recorder.
To do that we need another two include directives
#include <akonadi/itemfetchscope.h>
#include <akonadi/changerecorder.h>
and an additional line in the resource's constructor
changeRecorder()->itemFetchScope().fetchFullPayload();
This tells the base implementation that each item change should be fetched with full payload before delivering it to the methods covered in the following subsections.
Adding Items
In order to handle items being added to one of our collections, we need to implement the method itemAdded.
First we need another include directive for a helper class:
#include <krandom.h>
Now we can implement itemAdded like this:
void VCardDirResource::itemAdded( const Akonadi::Item &item, const Akonadi::Collection &collection )
{
const QString path = collection.remoteId();
KABC::Addressee addressee;
if ( item.hasPayload<KABC::Addressee>() )
addressee = item.payload<KABC::Addressee>();
if ( addressee.uid().isEmpty() )
addressee.setUid( KRandom::randomString( 10 ) );
QFile file( path + QLatin1Char( '/' ) + addressee.uid() + QLatin1String( ".vcf" ) );
if ( !file.open( QFile::WriteOnly ) )
return;
KABC::VCardConverter converter;
file.write( converter.createVCard( addressee ) );
if ( file.error() != QFile::NoError )
return;
Item newItem( item );
newItem.setRemoteId( file.fileName() );
newItem.setPayload<KABC::Addressee>( addressee );
changeCommitted( newItem );
}
As usual we get the path of the directory a specific collection maps to from the collection's remote identifier.
Then we check if the newly added item is already equiped with a payload, in our case a KABC::Addressee object. In either case we ensure that the object has an unique identifier by which we then use as the base name for the vCard file.
Finally we let Akonadi know that we have processed the item change and which remote identifier our backend has assigned to it.
Modifying Items
In our case handling item modifications is almost the same as handling item adding, however more sophisticated resource will most likely do different things depending on the parts parameter.
void VCardDirResource::itemChanged( const Akonadi::Item &item, const QSet<QByteArray> &parts )
{
Q_UNUSED( parts );
const QString fileName = item.remoteId();
KABC::Addressee addressee;
if ( item.hasPayload<KABC::Addressee>() )
addressee = item.payload<KABC::Addressee>();
if ( addressee.uid().isEmpty() )
addressee.setUid( KRandom::randomString( 10 ) );
QFile file( fileName );
if ( !file.open( QFile::WriteOnly ) )
return;
KABC::VCardConverter converter;
file.write( converter.createVCard( addressee ) );
if ( file.error() != QFile::NoError )
return;
Item newItem( item );
newItem.setPayload<KABC::Addressee>( addressee );
changeCommitted( newItem );
}
Removing Items
Since we have a one-to-one mapping of items to files, an item removal is as simple as removing the respective file:
void VCardDirResource::itemRemoved( const Akonadi::Item &item )
{
Q_UNUSED( item );
const QString fileName = item.remoteId();
QFile::remove( fileName );
changeCommitted( item );
}
Backend Changes
Of course changes to collections and items might also happen on the backend and in cases where the backend can send out change notifications, the resource can map these changes directly into its collection tree.
The APIs for that are exactly the same which any other client would use, i.e. Akonadi's job classes.
Testing
The Akonadi Test Framework allows to create self-contained test environments so developers do not risk impacting their normal Akonadi setup.
To install new resource to system run (might need root):
make install
or copy built files manually to the following locations:
$KDEDIRS/bin/akonadi_vcarddir_resource
$KDEDIRS/share/akonadi/agents/vcarddirresource.desktop
where $KDEDIRS points to your KDE installation prefix.
Then run Akonadi Configuration (search by this keyword in kickoff launcher) and click "Add..." button to add new resource - new resource should be there; or in KAddressBook add new address book - new resource should be available as address book type (restarting akonadi server might help if it does not get into this list immediately).
cleanup confusing sections and fix sections which contain a todo
Create tutorial about using the test suite.
Frequently Asked Questions and Tips
Base Class Methods
When is configure() called
ResourceBase::configure() is called under two circumstances.
- as part of the resource's creation process: when the Akonadi control process creates a resource, it will also call the resource's configure() method through D-Bus once the resource has registered itself.
- when another program explicitly requests it: since configure() is exposed as a D-Bus method, any other program can call it to request a resource's reconfiguration. End user applications might offer this functionality to allow the user to change resource specific settings.
What is the purpose of the window ID parameter of configure()?
The resource is a program on its own, i.e. it is not the same process as the end user application working with the data provided by the resource.
In an in-process scenario any configuration UI can specify one of the application's top level widgets as its parent, thus associating the configuration UI with that window.
Window managing systems such as KDE's KWin use this information to keep new windows from interrupting the user unless they are very likely a consequence of a user's action.
To make such an association across process boundaries, the calling application can send a platforms specific window identifier over to the resource which in tunr can then use it to specify it as a "parent" for its configuration UI like this:
ConfigDialog dialog;
KWindowSystem::setMainWindow( &dialog, windowId );
See KWindowSystem
Is retrieveItem() called in the order retrieveItems() passes the items?
The purpose of ResourceBase::retrieveItem() is to deliver additional item data, i.e. data not yet available in the Akonadi cache.
This can happen due to several different reasons, only one of them is retrieveItems() delivering just basic items.
An implementation should therefore always treat an invocation of this method as a single incidence unrelated to anything else.