1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2013 Utku Aydın <utkuaydin34@gmail.com>
4 //
5 
6 #include "BookmarkSyncManager.h"
7 
8 #include "GeoWriter.h"
9 #include "MarbleDirs.h"
10 #include "MarbleDebug.h"
11 #include "GeoDataParser.h"
12 #include "GeoDataFolder.h"
13 #include "GeoDataDocument.h"
14 #include "GeoDataLookAt.h"
15 #include "CloudSyncManager.h"
16 #include "GeoDataCoordinates.h"
17 #include "OwncloudSyncBackend.h"
18 #include "MarbleModel.h"
19 #include "BookmarkManager.h"
20 
21 #include <QFile>
22 #include <QBuffer>
23 #include <QJsonDocument>
24 #include <QJsonObject>
25 #include <QNetworkAccessManager>
26 #include <QNetworkReply>
27 #include <QTimer>
28 
29 namespace Marble {
30 
31 class DiffItem
32 {
33 public:
34     enum Action {
35         NoAction,
36         Created,
37         Changed,
38         Deleted
39     };
40 
41     enum Status {
42         Source,
43         Destination
44     };
45 
46     QString m_path;
47     Action m_action;
48     Status m_origin;
49     GeoDataPlacemark m_placemarkA;
50     GeoDataPlacemark m_placemarkB;
51 };
52 
53 class Q_DECL_HIDDEN BookmarkSyncManager::Private
54 {
55 public:
56     Private( BookmarkSyncManager* parent, CloudSyncManager *cloudSyncManager );
57 
58     BookmarkSyncManager* m_q;
59     CloudSyncManager *m_cloudSyncManager;
60 
61     QNetworkAccessManager m_network;
62     QString m_uploadEndpoint;
63     QString m_downloadEndpoint;
64     QString m_timestampEndpoint;
65 
66     QNetworkReply* m_uploadReply;
67     QNetworkReply* m_downloadReply;
68     QNetworkReply* m_timestampReply;
69 
70     QString m_cloudTimestamp;
71 
72     QString m_cachePath;
73     QString m_localBookmarksPath;
74     QString m_bookmarksTimestamp;
75 
76     QList<DiffItem> m_diffA;
77     QList<DiffItem> m_diffB;
78     QList<DiffItem> m_merged;
79     DiffItem m_conflictItem;
80 
81     BookmarkManager* m_bookmarkManager;
82     QTimer m_syncTimer;
83     bool m_bookmarkSyncEnabled;
84 
85     /**
86      * Returns an API endpoint
87      * @param endpoint Endpoint itself without server info
88      * @return Complete API URL as QUrl
89      */
90     QUrl endpointUrl( const QString &endpoint ) const;
91 
92     /**
93      * Uploads local bookmarks.kml to cloud.
94      */
95     void uploadBookmarks();
96 
97     /**
98      * Downloads bookmarks.kml from cloud.
99      */
100     void downloadBookmarks();
101 
102     /**
103      * Gets cloud bookmarks.kml's timestamp from cloud.
104      */
105     void downloadTimestamp();
106 
107     /**
108      * Compares cloud bookmarks.kml's timestamp to last synced bookmarks.kml's timestamp.
109      * @return true if cloud one is different from last synced one.
110      */
111     bool cloudBookmarksModified( const QString &cloudTimestamp ) const;
112 
113     /**
114      * Removes all KMLs in the cache except the
115      * one with youngest timestamp.
116      */
117     void clearCache();
118 
119     /**
120      * Finds the last synced bookmarks.kml file and returns its path
121      * @return Path of last synced bookmarks.kml file.
122      */
123     QString lastSyncedKmlPath() const;
124 
125     /**
126      * Gets all placemarks in a document as DiffItems, compares them to another document and puts the result in a list.
127      * @param document The document whose placemarks will be compared to another document's placemarks.
128      * @param other The document whose placemarks will be compared to the first document's placemarks.
129      * @param diffDirection Direction of comparison, e.g. must be DiffItem::Destination if direction is source to destination.
130      * @return A list of DiffItems
131      */
132     QList<DiffItem> getPlacemarks(GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection );
133 
134     /**
135      * Gets all placemarks in a document as DiffItems, compares them to another document and puts the result in a list.
136      * @param folder The folder whose placemarks will be compared to another document's placemarks.
137      * @param path Path of the folder.
138      * @param other The document whose placemarks will be compared to the first document's placemarks.
139      * @param diffDirection Direction of comparison, e.g. must be DiffItem::Destination if direction is source to destination.
140      * @return A list of DiffItems
141      */
142     QList<DiffItem> getPlacemarks( GeoDataFolder *folder, QString &path, GeoDataDocument *other, DiffItem::Status diffDirection );
143 
144     /**
145      * Finds the placemark which has the same coordinates with given bookmark
146      * @param container Container of placemarks which will be compared. Can be document or folder.
147      * @param bookmark The bookmark whose counterpart will be searched in the container.
148      * @return Counterpart of the given placemark.
149      */
150     const GeoDataPlacemark* findPlacemark( GeoDataContainer* container, const GeoDataPlacemark &bookmark ) const;
151 
152     /**
153      * Determines the status (created, deleted, changed or unchanged) of given DiffItem
154      * by comparing the item's placemark with placemarks of given GeoDataDocument.
155      * @param item The item whose status will be determined.
156      * @param document The document whose placemarks will be used to determine DiffItem's status.
157      */
158     void determineDiffStatus( DiffItem &item, GeoDataDocument* document ) const;
159 
160     /**
161      * Finds differences between two bookmark files.
162      * @param sourcePath Source bookmark
163      * @param destinationPath Destination bookmark
164      * @return A list of differences
165      */
166     QList<DiffItem> diff( QString &sourcePath, QString &destinationPath );
167     QList<DiffItem> diff( QString &sourcePath, QIODevice* destination );
168     QList<DiffItem> diff( QIODevice* source, QString &destinationPath );
169     QList<DiffItem> diff( QIODevice *source, QIODevice* destination );
170 
171     /**
172      * Merges two diff lists.
173      * @param diffListA First diff list.
174      * @param diffListB Second diff list.
175      * @return Merged DiffItems.
176      */
177     void merge();
178 
179     /**
180      * Creates GeoDataFolders using strings in path list.
181      * @param container Container which created GeoDataFolder will be attached to.
182      * @param pathList Names of folders. Note that each item will be the child of the previous one.
183      * @return A pointer to created folder.
184      */
185     GeoDataFolder* createFolders( GeoDataContainer *container, QStringList &pathList );
186 
187     /**
188      * Creates a GeoDataDocument using a list of DiffItems.
189      * @param mergedList DiffItems which will be used as placemarks.
190      * @return A pointer to created document.
191      */
192     GeoDataDocument* constructDocument( const QList<DiffItem> &mergedList );
193 
194     void saveDownloadedToCache( const QByteArray &kml );
195 
196     void parseTimestamp();
197     void copyLocalToCache();
198 
199     void continueSynchronization();
200     void completeSynchronization();
201     void completeMerge();
202     void completeUpload();
203 };
204 
Private(BookmarkSyncManager * parent,CloudSyncManager * cloudSyncManager)205 BookmarkSyncManager::Private::Private(BookmarkSyncManager *parent, CloudSyncManager *cloudSyncManager ) :
206   m_q( parent ),
207   m_cloudSyncManager( cloudSyncManager ),
208   m_bookmarkManager( nullptr ),
209   m_bookmarkSyncEnabled( false )
210 {
211     m_cachePath = MarbleDirs::localPath() + QLatin1String("/cloudsync/cache/bookmarks");
212     m_localBookmarksPath = MarbleDirs::localPath() + QLatin1String("/bookmarks/bookmarks.kml");
213     m_downloadEndpoint = "bookmarks/kml";
214     m_uploadEndpoint = "bookmarks/update";
215     m_timestampEndpoint = "bookmarks/timestamp";
216 }
217 
BookmarkSyncManager(CloudSyncManager * cloudSyncManager)218 BookmarkSyncManager::BookmarkSyncManager( CloudSyncManager *cloudSyncManager ) :
219   QObject(),
220   d( new Private( this, cloudSyncManager ) )
221 {
222     d->m_syncTimer.setInterval( 60 * 60 * 1000 ); // 1 hour. TODO: Make this configurable.
223     connect( &d->m_syncTimer, SIGNAL(timeout()), this, SLOT(startBookmarkSync()) );
224 }
225 
~BookmarkSyncManager()226 BookmarkSyncManager::~BookmarkSyncManager()
227 {
228     delete d;
229 }
230 
lastSync() const231 QDateTime BookmarkSyncManager::lastSync() const
232 {
233     const QString last = d->lastSyncedKmlPath();
234     if (last.isEmpty())
235         return QDateTime();
236     return QFileInfo(last).created();
237 }
238 
isBookmarkSyncEnabled() const239 bool BookmarkSyncManager::isBookmarkSyncEnabled() const
240 {
241     return d->m_bookmarkSyncEnabled;
242 }
243 
setBookmarkSyncEnabled(bool enabled)244 void BookmarkSyncManager::setBookmarkSyncEnabled( bool enabled )
245 {
246     bool const old_state = isBookmarkSyncEnabled();
247     d->m_bookmarkSyncEnabled = enabled;
248     if ( old_state != isBookmarkSyncEnabled() ) {
249         emit bookmarkSyncEnabledChanged( d->m_bookmarkSyncEnabled );
250         if ( isBookmarkSyncEnabled() ) {
251             startBookmarkSync();
252         }
253     }
254 }
255 
setBookmarkManager(BookmarkManager * manager)256 void BookmarkSyncManager::setBookmarkManager(BookmarkManager *manager)
257 {
258   d->m_bookmarkManager = manager;
259   connect( manager, SIGNAL(bookmarksChanged()), this, SLOT(startBookmarkSync()) );
260   startBookmarkSync();
261 }
262 
startBookmarkSync()263 void BookmarkSyncManager::startBookmarkSync()
264 {
265     if ( !d->m_cloudSyncManager->isSyncEnabled() || !isBookmarkSyncEnabled() )
266     {
267         return;
268     }
269 
270     d->m_syncTimer.start();
271     d->downloadTimestamp();
272 }
273 
endpointUrl(const QString & endpoint) const274 QUrl BookmarkSyncManager::Private::endpointUrl( const QString &endpoint ) const
275 {
276     return QUrl(m_cloudSyncManager->apiUrl().toString() + QLatin1Char('/') + endpoint);
277 }
278 
uploadBookmarks()279 void BookmarkSyncManager::Private::uploadBookmarks()
280 {
281     QByteArray data;
282     QByteArray lineBreak = "\r\n";
283     QString word = "----MarbleCloudBoundary";
284     QString boundary = QString( "--%0" ).arg( word );
285     QNetworkRequest request( endpointUrl( m_uploadEndpoint ) );
286     request.setHeader( QNetworkRequest::ContentTypeHeader, QString( "multipart/form-data; boundary=%0" ).arg( word ) );
287 
288     data.append( QString( boundary + lineBreak ).toUtf8() );
289     data.append( "Content-Disposition: form-data; name=\"bookmarks\"; filename=\"bookmarks.kml\"" + lineBreak );
290     data.append( "Content-Type: application/vnd.google-earth.kml+xml" + lineBreak + lineBreak );
291 
292     QFile bookmarksFile( m_localBookmarksPath );
293     if( !bookmarksFile.open( QFile::ReadOnly ) ) {
294         mDebug() << "Failed to open file" << bookmarksFile.fileName()
295                  <<  ". It is either missing or not readable.";
296         return;
297     }
298 
299     QByteArray kmlContent = bookmarksFile.readAll();
300     data.append( kmlContent + lineBreak + lineBreak );
301     data.append( QString( boundary ).toUtf8() );
302     bookmarksFile.close();
303 
304     m_uploadReply = m_network.post( request, data );
305     connect( m_uploadReply, SIGNAL(uploadProgress(qint64,qint64)),
306              m_q, SIGNAL(uploadProgress(qint64,qint64)) );
307     connect( m_uploadReply, SIGNAL(finished()),
308              m_q, SLOT(completeUpload()) );
309 }
310 
downloadBookmarks()311 void BookmarkSyncManager::Private::downloadBookmarks()
312 {
313     QNetworkRequest request( endpointUrl( m_downloadEndpoint ) );
314     m_downloadReply = m_network.get( request );
315     connect( m_downloadReply, SIGNAL(finished()),
316              m_q, SLOT(completeSynchronization()) );
317     connect( m_downloadReply, SIGNAL(downloadProgress(qint64,qint64)),
318              m_q, SIGNAL(downloadProgress(qint64,qint64)) );
319 }
320 
downloadTimestamp()321 void BookmarkSyncManager::Private::downloadTimestamp()
322 {
323     mDebug() << "Determining remote bookmark state.";
324     m_timestampReply = m_network.get( QNetworkRequest( endpointUrl( m_timestampEndpoint ) ) );
325     connect( m_timestampReply, SIGNAL(finished()),
326              m_q, SLOT(parseTimestamp()) );
327 }
328 
cloudBookmarksModified(const QString & cloudTimestamp) const329 bool BookmarkSyncManager::Private::cloudBookmarksModified( const QString &cloudTimestamp ) const
330 {
331     QStringList entryList = QDir( m_cachePath ).entryList(
332                 // TODO: replace with regex filter that only
333                 // allows timestamp filenames
334                 QStringList() << "*.kml",
335                 QDir::NoFilter, QDir::Name );
336     if( !entryList.isEmpty() ) {
337         QString lastSynced = entryList.last();
338         lastSynced.chop( 4 );
339         return cloudTimestamp != lastSynced;
340     } else {
341         return true; // That will let cloud one get downloaded.
342     }
343 }
344 
clearCache()345 void BookmarkSyncManager::Private::clearCache()
346 {
347     QDir cacheDir( m_cachePath );
348     QFileInfoList fileInfoList = cacheDir.entryInfoList(
349                 QStringList() << "*.kml",
350                 QDir::NoFilter, QDir::Name );
351     if( !fileInfoList.isEmpty() ) {
352         for ( const QFileInfo& fileInfo: fileInfoList ) {
353             QFile file( fileInfo.absoluteFilePath() );
354             bool removed = file.remove();
355             if( !removed ) {
356                 mDebug() << "Could not delete" << file.fileName() <<
357                          "Make sure you have sufficient permissions.";
358             }
359         }
360     }
361 }
362 
lastSyncedKmlPath() const363 QString BookmarkSyncManager::Private::lastSyncedKmlPath() const
364 {
365     QDir cacheDir( m_cachePath );
366     QFileInfoList fileInfoList = cacheDir.entryInfoList(
367                 QStringList() << "*.kml",
368                 QDir::NoFilter, QDir::Name );
369     if( !fileInfoList.isEmpty() ) {
370         return fileInfoList.last().absoluteFilePath();
371     } else {
372         return QString();
373     }
374 }
375 
getPlacemarks(GeoDataDocument * document,GeoDataDocument * other,DiffItem::Status diffDirection)376 QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks( GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection )
377 {
378     QList<DiffItem> diffItems;
379     for ( GeoDataFolder *folder: document->folderList() ) {
380         QString path = QString( "/%0" ).arg( folder->name() );
381         diffItems.append( getPlacemarks( folder, path, other, diffDirection ) );
382     }
383 
384     return diffItems;
385 }
386 
getPlacemarks(GeoDataFolder * folder,QString & path,GeoDataDocument * other,DiffItem::Status diffDirection)387 QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks( GeoDataFolder *folder, QString &path, GeoDataDocument *other, DiffItem::Status diffDirection )
388 {
389     QList<DiffItem> diffItems;
390     for ( GeoDataFolder *subFolder: folder->folderList() ) {
391         QString newPath = QString( "%0/%1" ).arg( path, subFolder->name() );
392         diffItems.append( getPlacemarks( subFolder, newPath, other, diffDirection ) );
393     }
394 
395     for( GeoDataPlacemark *placemark: folder->placemarkList() ) {
396         DiffItem diffItem;
397         diffItem.m_path = path;
398         diffItem.m_placemarkA = *placemark;
399         switch ( diffDirection ) {
400         case DiffItem::Source:
401             diffItem.m_origin = DiffItem::Destination;
402             break;
403         case DiffItem::Destination:
404             diffItem.m_origin = DiffItem::Source;
405             break;
406         default:
407             break;
408         }
409 
410         determineDiffStatus( diffItem, other );
411 
412         if( !( diffItem.m_action == DiffItem::NoAction && diffItem.m_origin == DiffItem::Destination )
413                 && !( diffItem.m_action == DiffItem::Changed && diffItem.m_origin == DiffItem::Source ) ) {
414             diffItems.append( diffItem );
415         }
416     }
417 
418     return diffItems;
419 }
420 
findPlacemark(GeoDataContainer * container,const GeoDataPlacemark & bookmark) const421 const GeoDataPlacemark* BookmarkSyncManager::Private::findPlacemark( GeoDataContainer* container, const GeoDataPlacemark &bookmark ) const
422 {
423     for( GeoDataPlacemark* placemark: container->placemarkList() ) {
424         if (EARTH_RADIUS * placemark->coordinate().sphericalDistanceTo(bookmark.coordinate()) <= 1) {
425             return placemark;
426         }
427     }
428 
429     for( GeoDataFolder* folder: container->folderList() ) {
430         const GeoDataPlacemark* placemark = findPlacemark( folder, bookmark );
431         if ( placemark ) {
432             return placemark;
433         }
434     }
435 
436     return nullptr;
437 }
438 
determineDiffStatus(DiffItem & item,GeoDataDocument * document) const439 void BookmarkSyncManager::Private::determineDiffStatus( DiffItem &item, GeoDataDocument *document ) const
440 {
441     const GeoDataPlacemark *match = findPlacemark( document, item.m_placemarkA );
442 
443     if( match != nullptr ) {
444         item.m_placemarkB = *match;
445         bool nameChanged = item.m_placemarkA.name() != item.m_placemarkB.name();
446         bool descChanged = item.m_placemarkA.description() != item.m_placemarkB.description();
447         bool lookAtChanged = item.m_placemarkA.lookAt()->latitude() != item.m_placemarkB.lookAt()->latitude() ||
448                 item.m_placemarkA.lookAt()->longitude() != item.m_placemarkB.lookAt()->longitude() ||
449                 item.m_placemarkA.lookAt()->altitude() != item.m_placemarkB.lookAt()->altitude() ||
450                 item.m_placemarkA.lookAt()->range() != item.m_placemarkB.lookAt()->range();
451         if(  nameChanged || descChanged || lookAtChanged ) {
452             item.m_action = DiffItem::Changed;
453         } else {
454             item.m_action = DiffItem::NoAction;
455         }
456     } else {
457         switch( item.m_origin ) {
458         case DiffItem::Source:
459             item.m_action = DiffItem::Deleted;
460             item.m_placemarkB = item.m_placemarkA; // for conflict purposes
461             break;
462         case DiffItem::Destination:
463             item.m_action = DiffItem::Created;
464             break;
465         }
466 
467     }
468 }
469 
diff(QString & sourcePath,QString & destinationPath)470 QList<DiffItem> BookmarkSyncManager::Private::diff( QString &sourcePath, QString &destinationPath )
471 {
472     QFile fileB( destinationPath );
473     if( !fileB.open( QFile::ReadOnly ) ) {
474         mDebug() << "Could not open file " << fileB.fileName();
475     }
476     return diff( sourcePath, &fileB );
477 }
478 
diff(QString & sourcePath,QIODevice * fileB)479 QList<DiffItem> BookmarkSyncManager::Private::diff( QString &sourcePath, QIODevice *fileB )
480 {
481     QFile fileA( sourcePath );
482     if( !fileA.open( QFile::ReadOnly ) ) {
483         mDebug() << "Could not open file " << fileA.fileName();
484     }
485 
486     return diff( &fileA, fileB );
487 }
488 
diff(QIODevice * source,QString & destinationPath)489 QList<DiffItem> BookmarkSyncManager::Private::diff( QIODevice *source, QString &destinationPath )
490 {
491     QFile fileB( destinationPath );
492     if( !fileB.open( QFile::ReadOnly ) ) {
493         mDebug() << "Could not open file " << fileB.fileName();
494     }
495 
496     return diff( source, &fileB );
497 }
498 
diff(QIODevice * fileA,QIODevice * fileB)499 QList<DiffItem> BookmarkSyncManager::Private::diff( QIODevice *fileA, QIODevice *fileB )
500 {
501     GeoDataParser parserA( GeoData_KML );
502     parserA.read( fileA );
503     GeoDataDocument *documentA = dynamic_cast<GeoDataDocument*>( parserA.releaseDocument() );
504 
505     GeoDataParser parserB( GeoData_KML );
506     parserB.read( fileB );
507     GeoDataDocument *documentB = dynamic_cast<GeoDataDocument*>( parserB.releaseDocument() );
508 
509     QList<DiffItem> diffItems = getPlacemarks( documentA, documentB, DiffItem::Destination ); // Compare old to new
510     diffItems.append( getPlacemarks( documentB, documentA, DiffItem::Source ) ); // Compare new to old
511 
512     // Compare paths
513     for( int i = 0; i < diffItems.count(); i++ ) {
514         for( int p = i + 1; p < diffItems.count(); p++ ) {
515             if( ( diffItems[i].m_origin == DiffItem::Source )
516                     && ( diffItems[i].m_action == DiffItem::NoAction )
517                     && ( EARTH_RADIUS * diffItems[i].m_placemarkA.coordinate().sphericalDistanceTo(diffItems[p].m_placemarkB.coordinate()) <= 1 )
518                     && ( EARTH_RADIUS * diffItems[i].m_placemarkB.coordinate().sphericalDistanceTo(diffItems[p].m_placemarkA.coordinate()) <= 1 )
519                     && ( diffItems[i].m_path != diffItems[p].m_path ) ) {
520                 diffItems[p].m_action = DiffItem::Changed;
521             }
522         }
523     }
524 
525     return diffItems;
526 }
527 
merge()528 void BookmarkSyncManager::Private::merge()
529 {
530     for( const DiffItem &itemA: m_diffA ) {
531         if( itemA.m_action == DiffItem::NoAction ) {
532             bool deleted = false;
533             bool changed = false;
534             DiffItem other;
535 
536             for( const DiffItem &itemB: m_diffB ) {
537                 if( EARTH_RADIUS * itemA.m_placemarkA.coordinate().sphericalDistanceTo(itemB.m_placemarkA.coordinate()) <= 1 ) {
538                     if( itemB.m_action == DiffItem::Deleted ) {
539                         deleted = true;
540                     } else if( itemB.m_action == DiffItem::Changed ) {
541                         changed = true;
542                         other = itemB;
543                     }
544                 }
545             }
546             if( changed ) {
547                 m_merged.append( other );
548             } else if( !deleted ) {
549                 m_merged.append( itemA );
550             }
551         } else if( itemA.m_action == DiffItem::Created ) {
552             m_merged.append( itemA );
553         } else if( itemA.m_action == DiffItem::Changed || itemA.m_action == DiffItem::Deleted ) {
554             bool conflict = false;
555             DiffItem other;
556 
557             for( const DiffItem &itemB: m_diffB ) {
558                 if (EARTH_RADIUS * itemA.m_placemarkB.coordinate().sphericalDistanceTo(itemB.m_placemarkB.coordinate()) <= 1) {
559                     if( ( itemA.m_action == DiffItem::Changed && ( itemB.m_action == DiffItem::Changed || itemB.m_action == DiffItem::Deleted ) )
560                             || ( itemA.m_action == DiffItem::Deleted && itemB.m_action == DiffItem::Changed ) ) {
561                         conflict = true;
562                         other = itemB;
563                     }
564                 }
565             }
566 
567             if( !conflict && itemA.m_action == DiffItem::Changed ) {
568                 m_merged.append( itemA );
569             } else if ( conflict ) {
570                 m_conflictItem = other;
571                 MergeItem *mergeItem = new MergeItem();
572                 mergeItem->setPathA( itemA.m_path );
573                 mergeItem->setPathB( other.m_path );
574                 mergeItem->setPlacemarkA( itemA.m_placemarkA );
575                 mergeItem->setPlacemarkB( other.m_placemarkA );
576 
577                 switch( itemA.m_action ) {
578                 case DiffItem::Changed:
579                     mergeItem->setActionA( MergeItem::Changed );
580                     break;
581                 case DiffItem::Deleted:
582                     mergeItem->setActionA( MergeItem::Deleted );
583                     break;
584                 default:
585                     break;
586                 }
587 
588                 switch( other.m_action ) {
589                 case DiffItem::Changed:
590                     mergeItem->setActionB( MergeItem::Changed );
591                     break;
592                 case DiffItem::Deleted:
593                     mergeItem->setActionB( MergeItem::Deleted );
594                     break;
595                 default:
596                     break;
597                 }
598 
599                 emit m_q->mergeConflict( mergeItem );
600                 return;
601             }
602         }
603 
604         if( !m_diffA.isEmpty() ) {
605             m_diffA.removeFirst();
606         }
607     }
608 
609     for( const DiffItem &itemB: m_diffB ) {
610         if( itemB.m_action == DiffItem::Created ) {
611             m_merged.append( itemB );
612         }
613     }
614 
615     completeMerge();
616 }
617 
createFolders(GeoDataContainer * container,QStringList & pathList)618 GeoDataFolder* BookmarkSyncManager::Private::createFolders( GeoDataContainer *container, QStringList &pathList )
619 {
620     GeoDataFolder *folder = nullptr;
621     if( pathList.count() > 0 ) {
622         QString name = pathList.takeFirst();
623 
624         for( GeoDataFolder *otherFolder: container->folderList() ) {
625             if( otherFolder->name() == name ) {
626                 folder = otherFolder;
627             }
628         }
629 
630         if( folder == nullptr ) {
631             folder = new GeoDataFolder();
632             folder->setName( name );
633             container->append( folder );
634         }
635 
636         if( pathList.count() == 0 ) {
637             return folder;
638         }
639     }
640 
641     return createFolders( folder, pathList );
642 }
643 
constructDocument(const QList<DiffItem> & mergedList)644 GeoDataDocument* BookmarkSyncManager::Private::constructDocument( const QList<DiffItem> &mergedList )
645 {
646     GeoDataDocument *document = new GeoDataDocument();
647     document->setName( tr( "Bookmarks" ) );
648 
649     for( const DiffItem &item: mergedList ) {
650         GeoDataPlacemark *placemark = new GeoDataPlacemark( item.m_placemarkA );
651         QStringList splitten = item.m_path.split(QLatin1Char('/'), QString::SkipEmptyParts);
652         GeoDataFolder *folder = createFolders( document, splitten );
653         folder->append( placemark );
654     }
655 
656     return document;
657 }
658 
resolveConflict(MergeItem * item)659 void BookmarkSyncManager::resolveConflict( MergeItem *item )
660 {
661     DiffItem diffItem;
662 
663     switch( item->resolution() ) {
664     case MergeItem::A:
665         if( !d->m_diffA.isEmpty() ) {
666             diffItem = d->m_diffA.first();
667             break;
668         }
669     case MergeItem::B:
670         diffItem = d->m_conflictItem;
671         break;
672     default:
673         return; // It shouldn't happen.
674     }
675 
676     if( diffItem.m_action != DiffItem::Deleted ) {
677         d->m_merged.append( diffItem );
678     }
679 
680     if( !d->m_diffA.isEmpty() ) {
681         d->m_diffA.removeFirst();
682     }
683 
684     d->merge();
685 }
686 
saveDownloadedToCache(const QByteArray & kml)687 void BookmarkSyncManager::Private::saveDownloadedToCache( const QByteArray &kml )
688 {
689     QString localBookmarksDir = m_localBookmarksPath;
690     QDir().mkdir( localBookmarksDir.remove( "bookmarks.kml" ) );
691     QFile bookmarksFile( m_localBookmarksPath );
692     if( !bookmarksFile.open( QFile::ReadWrite ) ) {
693         mDebug() << "Failed to open file" << bookmarksFile.fileName()
694                  <<  ". It is either missing or not readable.";
695         return;
696     }
697 
698     bookmarksFile.write( kml );
699     bookmarksFile.close();
700     copyLocalToCache();
701 }
702 
parseTimestamp()703 void BookmarkSyncManager::Private::parseTimestamp()
704 {
705     QJsonDocument jsonDoc = QJsonDocument::fromJson(m_timestampReply->readAll());
706     QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data"));
707 
708     m_cloudTimestamp = dataValue.toString();
709     mDebug() << "Remote bookmark timestamp is " << m_cloudTimestamp;
710     continueSynchronization();
711 }
copyLocalToCache()712 void BookmarkSyncManager::Private::copyLocalToCache()
713 {
714     QDir().mkpath( m_cachePath );
715     clearCache();
716 
717     QFile bookmarksFile( m_localBookmarksPath );
718     bookmarksFile.copy( QString( "%0/%1.kml" ).arg( m_cachePath, m_cloudTimestamp ) );
719 }
720 
721 // Bookmark synchronization steps
continueSynchronization()722 void BookmarkSyncManager::Private::continueSynchronization()
723 {
724     bool cloudModified = cloudBookmarksModified( m_cloudTimestamp );
725     if( cloudModified ) {
726         downloadBookmarks();
727     } else {
728         QString lastSyncedPath = lastSyncedKmlPath();
729         if( lastSyncedPath.isEmpty() ) {
730             mDebug() << "Never synced. Uploading bookmarks.";
731             uploadBookmarks();
732         } else {
733             QList<DiffItem> diffList = diff( lastSyncedPath, m_localBookmarksPath );
734             bool localModified = false;
735             for( const DiffItem &item: diffList ) {
736                 if( item.m_action != DiffItem::NoAction ) {
737                     localModified = true;
738                 }
739             }
740 
741             if( localModified ) {
742                 mDebug() << "Local modifications, uploading.";
743                 uploadBookmarks();
744             }
745         }
746     }
747 }
748 
completeSynchronization()749 void BookmarkSyncManager::Private::completeSynchronization()
750 {
751     mDebug() << "Merging remote and local bookmark file";
752     QString lastSyncedPath = lastSyncedKmlPath();
753     QFile localBookmarksFile( m_localBookmarksPath );
754     QByteArray result = m_downloadReply->readAll();
755     QBuffer buffer( &result );
756     buffer.open( QIODevice::ReadOnly );
757 
758     if( lastSyncedPath.isEmpty() ) {
759         if( localBookmarksFile.exists() ) {
760             mDebug() << "Conflict between remote bookmarks and local ones";
761             m_diffA = diff( &buffer, m_localBookmarksPath );
762             m_diffB = diff( m_localBookmarksPath, &buffer );
763         } else {
764             saveDownloadedToCache( result );
765             return;
766         }
767     }
768     else
769     {
770         m_diffA = diff( lastSyncedPath, m_localBookmarksPath );
771         m_diffB = diff( lastSyncedPath, &buffer );
772     }
773 
774     m_merged.clear();
775     merge();
776 }
777 
completeMerge()778 void BookmarkSyncManager::Private::completeMerge()
779 {
780     QFile localBookmarksFile( m_localBookmarksPath );
781     GeoDataDocument *doc = constructDocument( m_merged );
782     GeoWriter writer;
783     localBookmarksFile.remove();
784     localBookmarksFile.open( QFile::ReadWrite );
785     writer.write( &localBookmarksFile, doc );
786     localBookmarksFile.close();
787     uploadBookmarks();
788 }
789 
completeUpload()790 void BookmarkSyncManager::Private::completeUpload()
791 {
792     QJsonDocument jsonDoc = QJsonDocument::fromJson(m_uploadReply->readAll());
793     QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data"));
794 
795     m_cloudTimestamp = dataValue.toString();
796     mDebug() << "Uploaded bookmarks to remote server. Timestamp is " << m_cloudTimestamp;
797     copyLocalToCache();
798     emit m_q->syncComplete();
799 }
800 
801 }
802 
803 #include "moc_BookmarkSyncManager.cpp"
804