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 | |
36 | using namespace Akonadi; |
37 | |
38 | template <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 | |
47 | QTextStream& operator<<( QTextStream &s, const QStringList &list ) |
48 | { |
49 | s << '(' << list.join( ", " ) << ')'; |
50 | return s; |
51 | } |
52 | |
53 | XmlOperations::XmlOperations(QObject* parent) : |
54 | QObject( parent ), |
55 | mCollectionFields( 0xFF ), |
56 | mCollectionKey( RemoteId ), |
57 | mItemFields( 0xFF ), |
58 | mItemKey( ItemRemoteId ), |
59 | mNormalizeRemoteIds( false ) |
60 | { |
61 | } |
62 | |
63 | XmlOperations::~XmlOperations() |
64 | { |
65 | } |
66 | |
67 | Item XmlOperations::getItemByRemoteId(const QString& rid) |
68 | { |
69 | return mDocument.itemByRemoteId( rid, true); |
70 | } |
71 | |
72 | Collection XmlOperations::getCollectionByRemoteId(const QString& rid) |
73 | { |
74 | return mDocument.collectionByRemoteId(rid); |
75 | } |
76 | |
77 | void 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 | |
88 | void XmlOperations::setRootCollections(const Collection::List& roots) |
89 | { |
90 | mRoots = roots; |
91 | } |
92 | |
93 | void 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 | |
102 | QString XmlOperations::lastError() const |
103 | { |
104 | return mErrorMsg; |
105 | } |
106 | |
107 | void XmlOperations::setCollectionKey(XmlOperations::CollectionField field) |
108 | { |
109 | mCollectionKey = field; |
110 | } |
111 | |
112 | void 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 | |
118 | void XmlOperations::ignoreCollectionField(XmlOperations::CollectionField field) |
119 | { |
120 | mCollectionFields = mCollectionFields & ~field; |
121 | } |
122 | |
123 | void 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 | |
129 | void XmlOperations::setItemKey(XmlOperations::ItemField field) |
130 | { |
131 | mItemKey = field; |
132 | } |
133 | |
134 | void 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 | |
143 | void XmlOperations::ignoreItemField(XmlOperations::ItemField field) |
144 | { |
145 | mItemFields = mItemFields & ~field; |
146 | } |
147 | |
148 | void 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 | |
157 | void XmlOperations::setNormalizeRemoteIds(bool enable) |
158 | { |
159 | mNormalizeRemoteIds = enable; |
160 | } |
161 | |
162 | bool 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 | |
185 | void XmlOperations::assertEqual() |
186 | { |
187 | if ( !compare() ) |
188 | Test::instance()->fail( lastError() ); |
189 | } |
190 | |
191 | static 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 | |
202 | Collection 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 | |
213 | Item 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 | |
221 | bool 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 | |
266 | bool 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 | |
302 | bool 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 | |
321 | bool 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 | |
337 | bool 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 | |
378 | bool 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 | |
389 | bool 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 | |