1/*
2 Copyright (c) 2009 Volker Krause <vkrause@kde.org>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "xmloperations.h"
21#include "global.h"
22#include "test.h"
23
24#include <akonadi/collectionfetchjob.h>
25#include <akonadi/collectionfetchscope.h>
26#include <akonadi/itemfetchjob.h>
27#include <akonadi/itemfetchscope.h>
28#include <akonadi/xml/xmlwritejob.h>
29
30#include <KDebug>
31
32#include <QDir>
33#include <QFileInfo>
34#include <QStringList>
35
36using namespace Akonadi;
37
38template <typename T> QTextStream& operator<<( QTextStream &s, const QSet<T> &set )
39{
40 s << '{';
41 foreach ( const T &element, set )
42 s << element << ", ";
43 s << '}';
44 return s;
45}
46
47QTextStream& operator<<( QTextStream &s, const QStringList &list )
48{
49 s << '(' << list.join( ", " ) << ')';
50 return s;
51}
52
53XmlOperations::XmlOperations(QObject* parent) :
54 QObject( parent ),
55 mCollectionFields( 0xFF ),
56 mCollectionKey( RemoteId ),
57 mItemFields( 0xFF ),
58 mItemKey( ItemRemoteId ),
59 mNormalizeRemoteIds( false )
60{
61}
62
63XmlOperations::~XmlOperations()
64{
65}
66
67Item XmlOperations::getItemByRemoteId(const QString& rid)
68{
69 return mDocument.itemByRemoteId( rid, true);
70}
71
72Collection XmlOperations::getCollectionByRemoteId(const QString& rid)
73{
74 return mDocument.collectionByRemoteId(rid);
75}
76
77void XmlOperations::setRootCollections(const QString& resourceId)
78{
79 CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::FirstLevel, this );
80 job->fetchScope().setAncestorRetrieval( CollectionFetchScope::All );
81 job->fetchScope().setResource( resourceId );
82 if ( job->exec() )
83 setRootCollections( job->collections() );
84 else
85 mErrorMsg = job->errorText();
86}
87
88void XmlOperations::setRootCollections(const Collection::List& roots)
89{
90 mRoots = roots;
91}
92
93void XmlOperations::setXmlFile(const QString& fileName)
94{
95 mFileName = fileName;
96 if ( QFileInfo( fileName ).isAbsolute() )
97 mDocument.loadFile( fileName );
98 else
99 mDocument.loadFile( Global::basePath() + QDir::separator() + fileName );
100}
101
102QString XmlOperations::lastError() const
103{
104 return mErrorMsg;
105}
106
107void XmlOperations::setCollectionKey(XmlOperations::CollectionField field)
108{
109 mCollectionKey = field;
110}
111
112void XmlOperations::setCollectionKey(const QString& fieldName)
113{
114 const QMetaEnum me = metaObject()->enumerator( metaObject()->indexOfEnumerator( "CollectionField" ) );
115 setCollectionKey( static_cast<CollectionField>( me.keyToValue( fieldName.toLatin1() ) ) );
116}
117
118void XmlOperations::ignoreCollectionField(XmlOperations::CollectionField field)
119{
120 mCollectionFields = mCollectionFields & ~field;
121}
122
123void XmlOperations::ignoreCollectionField(const QString& fieldName)
124{
125 const QMetaEnum me = metaObject()->enumerator( metaObject()->indexOfEnumerator( "CollectionField" ) );
126 ignoreCollectionField( static_cast<CollectionField>( me.keyToValue( fieldName.toLatin1() ) ) );
127}
128
129void XmlOperations::setItemKey(XmlOperations::ItemField field)
130{
131 mItemKey = field;
132}
133
134void XmlOperations::setItemKey(const QString& _fieldName)
135{
136 QString fieldName = _fieldName;
137 if ( !fieldName.startsWith( QLatin1String ( "Item" ) ) )
138 fieldName.prepend( "Item" );
139 const QMetaEnum me = metaObject()->enumerator( metaObject()->indexOfEnumerator( "ItemField" ) );
140 setItemKey( static_cast<ItemField>( me.keyToValue( fieldName.toLatin1() ) ) );
141}
142
143void XmlOperations::ignoreItemField(XmlOperations::ItemField field)
144{
145 mItemFields = mItemFields & ~field;
146}
147
148void XmlOperations::ignoreItemField(const QString& _fieldName)
149{
150 QString fieldName = _fieldName;
151 if ( !fieldName.startsWith( "Item" ) )
152 fieldName.prepend( "Item" );
153 const QMetaEnum me = metaObject()->enumerator( metaObject()->indexOfEnumerator( "ItemField" ) );
154 ignoreItemField( static_cast<ItemField>( me.keyToValue( fieldName.toLatin1() ) ) );
155}
156
157void XmlOperations::setNormalizeRemoteIds(bool enable)
158{
159 mNormalizeRemoteIds = enable;
160}
161
162bool XmlOperations::compare()
163{
164 if ( !mDocument.isValid() ) {
165 mErrorMsg = mDocument.lastError();
166 return false;
167 }
168
169 if ( mRoots.isEmpty() ) {
170 if ( !mErrorMsg.isEmpty() )
171 mErrorMsg = QLatin1String( "No root collections specified." );
172 return false;
173 }
174
175 const Collection::List docRoots = mDocument.childCollections( Collection::root() );
176 if ( compareCollections( mRoots, docRoots ) )
177 return true;
178
179 XmlWriteJob *xmlJob = new XmlWriteJob( mRoots, mFileName + ".actual", this );
180 if ( !xmlJob->exec() )
181 kError() << xmlJob->errorText();
182 return false;
183}
184
185void XmlOperations::assertEqual()
186{
187 if ( !compare() )
188 Test::instance()->fail( lastError() );
189}
190
191static QString normalizeRemoteId( const QString &in )
192{
193 QString out( in );
194 if ( in.startsWith( Global:: basePath() ) ) {
195 out = in.mid( Global::basePath().length() );
196 if ( out.startsWith( QDir::separator() ) )
197 out = out.mid( 1 );
198 }
199 return out;
200}
201
202Collection XmlOperations::normalizeCollection( const Collection &in ) const
203{
204 Collection out( in );
205 if ( mNormalizeRemoteIds )
206 out.setRemoteId( normalizeRemoteId( in.remoteId() ) );
207 QStringList l = in.contentMimeTypes();
208 std::sort( l.begin(), l.end() );
209 out.setContentMimeTypes( l );
210 return out;
211}
212
213Item XmlOperations::normalizeItem( const Item& in ) const
214{
215 Item out( in );
216 if ( mNormalizeRemoteIds )
217 out.setRemoteId( normalizeRemoteId( in.remoteId() ) );
218 return out;
219}
220
221bool XmlOperations::compareCollections(const Collection::List& _cols, const Collection::List& _refCols)
222{
223 Collection::List cols;
224 foreach ( const Collection &c, _cols )
225 cols.append( normalizeCollection( c ) );
226 Collection::List refCols;
227 foreach ( const Collection &c, _refCols )
228 refCols.append( normalizeCollection( c ) );
229
230 switch ( mCollectionKey ) {
231 case RemoteId:
232 sortEntityList( cols, &Collection::remoteId );
233 sortEntityList( refCols, &Collection::remoteId );
234 break;
235 case Name:
236 sortEntityList( cols, &Collection::name );
237 sortEntityList( refCols, &Collection::name );
238 break;
239 case None:
240 break;
241 default:
242 Q_ASSERT( false );
243 }
244
245 for ( int i = 0; i < cols.count(); ++i ) {
246 const Collection col = cols.at( i );
247 if ( refCols.count() <= i ) {
248 mErrorMsg = QString::fromLatin1( "Additional collection with remote id '%1' was found." ).arg( col.remoteId() );
249 return false;
250 }
251
252 const Collection refCol = refCols.at( i );
253 if ( !compareCollection( col, refCol ) )
254 return false;
255 }
256
257 if ( refCols.count() != cols.count() ) {
258 const Collection refCol = refCols.at( cols.count() );
259 mErrorMsg = QString::fromLatin1( "Collection with remote id '%1' is missing." ).arg( refCol.remoteId() );
260 return false;
261 }
262
263 return true;
264}
265
266bool XmlOperations::compareCollection(const Collection& col, const Collection& refCol)
267{
268 // compare the two collections
269 if ( !compareValue( col, refCol, &Collection::remoteId, RemoteId ) ||
270 !compareValue( col, refCol, &Collection::contentMimeTypes, ContentMimeType ) ||
271 !compareValue( col, refCol, &Collection::name, Name ) )
272 return false;
273
274 if ( (mCollectionFields & Attributes) && !compareAttributes( col, refCol ) )
275 return false;
276
277 // compare child items
278 ItemFetchJob *ijob = new ItemFetchJob( col, this );
279 ijob->fetchScope().fetchAllAttributes( true );
280 ijob->fetchScope().fetchFullPayload( true );
281 if ( !ijob->exec() ) {
282 mErrorMsg = ijob->errorText();
283 return false;
284 }
285 const Item::List items = ijob->items();
286 const Item::List refItems = mDocument.items( refCol, true );
287 if ( !compareItems( items, refItems ) )
288 return false;
289
290 // compare child collections
291 CollectionFetchJob *cjob = new CollectionFetchJob( col, CollectionFetchJob::FirstLevel, this );
292 cjob->fetchScope().setAncestorRetrieval( CollectionFetchScope::All );
293 if ( !cjob->exec() ) {
294 mErrorMsg = cjob->errorText();
295 return false;
296 }
297 const Collection::List cols = cjob->collections();
298 const Collection::List refCols = mDocument.childCollections( refCol );
299 return compareCollections( cols, refCols );
300}
301
302bool XmlOperations::hasItem(const Item& _item, const Collection& _col)
303{
304 ItemFetchJob *ijob = new ItemFetchJob( _col, this );
305 ijob->fetchScope().fetchAllAttributes( true );
306 ijob->fetchScope().fetchFullPayload( true );
307 if ( !ijob->exec() ) {
308 mErrorMsg = ijob->errorText();
309 return false;
310 }
311 const Item::List items = ijob->items();
312
313 for( int i = 0; i < items.count(); ++i) {
314 if( _item == items.at(i) ) {
315 return true;
316 }
317 }
318 return false;
319}
320
321bool XmlOperations::hasItem(const Item& _item, const QString& rid)
322{
323 CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::FirstLevel, this );
324 Collection::List colist;
325
326 if ( job->exec() )
327 colist = job->collections() ;
328 foreach( const Collection &collection, colist ) {
329 if(rid == collection.remoteId()) {
330 return hasItem(_item, collection);
331 }
332 }
333 return false;
334}
335
336
337bool XmlOperations::compareItems(const Item::List& _items, const Item::List& _refItems)
338{
339 Item::List items;
340 foreach ( const Item &i, _items )
341 items.append( normalizeItem( i ) );
342 Item::List refItems;
343 foreach ( const Item &i, _refItems )
344 refItems.append( normalizeItem( i ) );
345
346 switch ( mItemKey ) {
347 case ItemRemoteId:
348 sortEntityList( items, &Item::remoteId );
349 sortEntityList( refItems, &Item::remoteId );
350 break;
351 case ItemNone:
352 break;
353 default:
354 Q_ASSERT( false );
355 }
356
357 for ( int i = 0; i < items.count(); ++i ) {
358 const Item item = items.at( i );
359 if ( refItems.count() <= i ) {
360 mErrorMsg = QString::fromLatin1( "Additional item with remote id '%1' was found." ).arg( item.remoteId() );
361 return false;
362 }
363
364 const Item refItem = refItems.at( i );
365 if ( !compareItem( item, refItem ) )
366 return false;
367 }
368
369 if ( refItems.count() != items.count() ) {
370 const Item refItem = refItems.at( items.count() );
371 mErrorMsg = QString::fromLatin1( "Item with remote id '%1' is missing." ).arg( refItem.remoteId() );
372 return false;
373 }
374
375 return true;
376}
377
378bool XmlOperations::compareItem(const Item& item, const Item& refItem)
379{
380 if ( !compareValue( item, refItem, &Item::remoteId, ItemRemoteId ) ||
381 !compareValue( item, refItem, &Item::mimeType, ItemMimeType ) ||
382 !compareValue( item, refItem, &Item::flags, ItemFlags ) ||
383 !compareValue( item, refItem, &Item::payloadData, ItemPayload ) )
384 return false;
385
386 return compareAttributes( item, refItem );
387}
388
389bool XmlOperations::compareAttributes(const Entity& entity, const Entity& refEntity)
390{
391 Attribute::List attrs = entity.attributes();
392 Attribute::List refAttrs = refEntity.attributes();
393 std::sort( attrs.begin(), attrs.end(), boost::bind( &Attribute::type, _1 ) < boost::bind( &Attribute::type, _2 ) );
394 std::sort( refAttrs.begin(), refAttrs.end(), boost::bind( &Attribute::type, _1 ) < boost::bind( &Attribute::type, _2 ) );
395
396 for ( int i = 0; i < attrs.count(); ++i ) {
397 Attribute* attr = attrs.at( i );
398 if ( refAttrs.count() <= i ) {
399 mErrorMsg = QString::fromLatin1( "Additional attribute of type '%1' for object with remote id '%2' was found." )
400 .arg( QString::fromLatin1( attr->type() ) ).arg( entity.remoteId() );
401 return false;
402 }
403
404 Attribute* refAttr = refAttrs.at( i );
405 if ( attr->type() != refAttr->type() ) {
406 mErrorMsg = QString::fromLatin1( "Object with remote id '%1' misses attribute of type '%2'." )
407 .arg( entity.remoteId() ).arg( QString::fromLatin1( refAttr->type() ) );
408 return false;
409 }
410
411 bool result = compareValue( attr->serialized(), refAttr->serialized() );
412 if ( !result ) {
413 mErrorMsg.prepend( QString::fromLatin1( "Object with remote id '%1' differs in attribute '%2':\n" )
414 .arg( entity.remoteId() ).arg( QString::fromLatin1( attr->type() ) ) );
415 return false;
416 }
417 }
418
419 if ( refAttrs.count() != attrs.count() ) {
420 Attribute* refAttr = refAttrs.at( attrs.count() );
421 mErrorMsg = QString::fromLatin1( "Object with remote id '%1' misses attribute of type '%2'." )
422 .arg( entity.remoteId() ).arg( QString::fromLatin1( refAttr->type() ) );;
423 return false;
424 }
425
426 return true;
427}
428
429