File indexing completed on 2024-05-05 03:49:14

0001 // SPDX-License-Identifier: LGPL-2.1-or-later
0002 //
0003 // SPDX-FileCopyrightText: 2013 Utku Aydın <utkuaydin34@gmail.com>
0004 //
0005 
0006 #include "BookmarkSyncManager.h"
0007 
0008 #include "GeoWriter.h"
0009 #include "MarbleDirs.h"
0010 #include "MarbleDebug.h"
0011 #include "GeoDataParser.h"
0012 #include "GeoDataFolder.h"
0013 #include "GeoDataDocument.h"
0014 #include "GeoDataLookAt.h"
0015 #include "CloudSyncManager.h"
0016 #include "GeoDataCoordinates.h"
0017 #include "OwncloudSyncBackend.h"
0018 #include "MarbleModel.h"
0019 #include "BookmarkManager.h"
0020 
0021 #include <QFile>
0022 #include <QBuffer>
0023 #include <QJsonDocument>
0024 #include <QJsonObject>
0025 #include <QNetworkAccessManager>
0026 #include <QNetworkReply>
0027 #include <QTimer>
0028 
0029 namespace Marble {
0030 
0031 class DiffItem
0032 {
0033 public:
0034     enum Action {
0035         NoAction,
0036         Created,
0037         Changed,
0038         Deleted
0039     };
0040 
0041     enum Status {
0042         Source,
0043         Destination
0044     };
0045 
0046     QString m_path;
0047     Action m_action;
0048     Status m_origin;
0049     GeoDataPlacemark m_placemarkA;
0050     GeoDataPlacemark m_placemarkB;
0051 };
0052 
0053 class Q_DECL_HIDDEN BookmarkSyncManager::Private
0054 {
0055 public:
0056     Private( BookmarkSyncManager* parent, CloudSyncManager *cloudSyncManager );
0057 
0058     BookmarkSyncManager* m_q;
0059     CloudSyncManager *m_cloudSyncManager;
0060 
0061     QNetworkAccessManager m_network;
0062     QString m_uploadEndpoint;
0063     QString m_downloadEndpoint;
0064     QString m_timestampEndpoint;
0065 
0066     QNetworkReply* m_uploadReply;
0067     QNetworkReply* m_downloadReply;
0068     QNetworkReply* m_timestampReply;
0069 
0070     QString m_cloudTimestamp;
0071 
0072     QString m_cachePath;
0073     QString m_localBookmarksPath;
0074     QString m_bookmarksTimestamp;
0075 
0076     QList<DiffItem> m_diffA;
0077     QList<DiffItem> m_diffB;
0078     QList<DiffItem> m_merged;
0079     DiffItem m_conflictItem;
0080 
0081     BookmarkManager* m_bookmarkManager;
0082     QTimer m_syncTimer;
0083     bool m_bookmarkSyncEnabled;
0084 
0085     /**
0086      * Returns an API endpoint
0087      * @param endpoint Endpoint itself without server info
0088      * @return Complete API URL as QUrl
0089      */
0090     QUrl endpointUrl( const QString &endpoint ) const;
0091 
0092     /**
0093      * Uploads local bookmarks.kml to cloud.
0094      */
0095     void uploadBookmarks();
0096 
0097     /**
0098      * Downloads bookmarks.kml from cloud.
0099      */
0100     void downloadBookmarks();
0101 
0102     /**
0103      * Gets cloud bookmarks.kml's timestamp from cloud.
0104      */
0105     void downloadTimestamp();
0106 
0107     /**
0108      * Compares cloud bookmarks.kml's timestamp to last synced bookmarks.kml's timestamp.
0109      * @return true if cloud one is different from last synced one.
0110      */
0111     bool cloudBookmarksModified( const QString &cloudTimestamp ) const;
0112 
0113     /**
0114      * Removes all KMLs in the cache except the
0115      * one with youngest timestamp.
0116      */
0117     void clearCache();
0118 
0119     /**
0120      * Finds the last synced bookmarks.kml file and returns its path
0121      * @return Path of last synced bookmarks.kml file.
0122      */
0123     QString lastSyncedKmlPath() const;
0124 
0125     /**
0126      * Gets all placemarks in a document as DiffItems, compares them to another document and puts the result in a list.
0127      * @param document The document whose placemarks will be compared to another document's placemarks.
0128      * @param other The document whose placemarks will be compared to the first document's placemarks.
0129      * @param diffDirection Direction of comparison, e.g. must be DiffItem::Destination if direction is source to destination.
0130      * @return A list of DiffItems
0131      */
0132     QList<DiffItem> getPlacemarks(GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection );
0133 
0134     /**
0135      * Gets all placemarks in a document as DiffItems, compares them to another document and puts the result in a list.
0136      * @param folder The folder whose placemarks will be compared to another document's placemarks.
0137      * @param path Path of the folder.
0138      * @param other The document whose placemarks will be compared to the first document's placemarks.
0139      * @param diffDirection Direction of comparison, e.g. must be DiffItem::Destination if direction is source to destination.
0140      * @return A list of DiffItems
0141      */
0142     QList<DiffItem> getPlacemarks( GeoDataFolder *folder, QString &path, GeoDataDocument *other, DiffItem::Status diffDirection );
0143 
0144     /**
0145      * Finds the placemark which has the same coordinates with given bookmark
0146      * @param container Container of placemarks which will be compared. Can be document or folder.
0147      * @param bookmark The bookmark whose counterpart will be searched in the container.
0148      * @return Counterpart of the given placemark.
0149      */
0150     const GeoDataPlacemark* findPlacemark( GeoDataContainer* container, const GeoDataPlacemark &bookmark ) const;
0151 
0152     /**
0153      * Determines the status (created, deleted, changed or unchanged) of given DiffItem
0154      * by comparing the item's placemark with placemarks of given GeoDataDocument.
0155      * @param item The item whose status will be determined.
0156      * @param document The document whose placemarks will be used to determine DiffItem's status.
0157      */
0158     void determineDiffStatus( DiffItem &item, GeoDataDocument* document ) const;
0159 
0160     /**
0161      * Finds differences between two bookmark files.
0162      * @param sourcePath Source bookmark
0163      * @param destinationPath Destination bookmark
0164      * @return A list of differences
0165      */
0166     QList<DiffItem> diff( QString &sourcePath, QString &destinationPath );
0167     QList<DiffItem> diff( QString &sourcePath, QIODevice* destination );
0168     QList<DiffItem> diff( QIODevice* source, QString &destinationPath );
0169     QList<DiffItem> diff( QIODevice *source, QIODevice* destination );
0170 
0171     /**
0172      * Merges two diff lists.
0173      * @param diffListA First diff list.
0174      * @param diffListB Second diff list.
0175      * @return Merged DiffItems.
0176      */
0177     void merge();
0178 
0179     /**
0180      * Creates GeoDataFolders using strings in path list.
0181      * @param container Container which created GeoDataFolder will be attached to.
0182      * @param pathList Names of folders. Note that each item will be the child of the previous one.
0183      * @return A pointer to created folder.
0184      */
0185     GeoDataFolder* createFolders( GeoDataContainer *container, QStringList &pathList );
0186 
0187     /**
0188      * Creates a GeoDataDocument using a list of DiffItems.
0189      * @param mergedList DiffItems which will be used as placemarks.
0190      * @return A pointer to created document.
0191      */
0192     GeoDataDocument* constructDocument( const QList<DiffItem> &mergedList );
0193 
0194     void saveDownloadedToCache( const QByteArray &kml );
0195 
0196     void parseTimestamp();
0197     void copyLocalToCache();
0198 
0199     void continueSynchronization();
0200     void completeSynchronization();
0201     void completeMerge();
0202     void completeUpload();
0203 };
0204 
0205 BookmarkSyncManager::Private::Private(BookmarkSyncManager *parent, CloudSyncManager *cloudSyncManager ) :
0206   m_q( parent ),
0207   m_cloudSyncManager( cloudSyncManager ),
0208   m_bookmarkManager( nullptr ),
0209   m_bookmarkSyncEnabled( false )
0210 {
0211     m_cachePath = MarbleDirs::localPath() + QLatin1String("/cloudsync/cache/bookmarks");
0212     m_localBookmarksPath = MarbleDirs::localPath() + QLatin1String("/bookmarks/bookmarks.kml");
0213     m_downloadEndpoint = "bookmarks/kml";
0214     m_uploadEndpoint = "bookmarks/update";
0215     m_timestampEndpoint = "bookmarks/timestamp";
0216 }
0217 
0218 BookmarkSyncManager::BookmarkSyncManager( CloudSyncManager *cloudSyncManager ) :
0219   QObject(),
0220   d( new Private( this, cloudSyncManager ) )
0221 {
0222     d->m_syncTimer.setInterval( 60 * 60 * 1000 ); // 1 hour. TODO: Make this configurable.
0223     connect( &d->m_syncTimer, SIGNAL(timeout()), this, SLOT(startBookmarkSync()) );
0224 }
0225 
0226 BookmarkSyncManager::~BookmarkSyncManager()
0227 {
0228     delete d;
0229 }
0230 
0231 QDateTime BookmarkSyncManager::lastSync() const
0232 {
0233     const QString last = d->lastSyncedKmlPath();
0234     if (last.isEmpty())
0235         return QDateTime();
0236     return QFileInfo(last).metadataChangeTime();
0237 }
0238 
0239 bool BookmarkSyncManager::isBookmarkSyncEnabled() const
0240 {
0241     return d->m_bookmarkSyncEnabled;
0242 }
0243 
0244 void BookmarkSyncManager::setBookmarkSyncEnabled( bool enabled )
0245 {
0246     bool const old_state = isBookmarkSyncEnabled();
0247     d->m_bookmarkSyncEnabled = enabled;
0248     if ( old_state != isBookmarkSyncEnabled() ) {
0249         emit bookmarkSyncEnabledChanged( d->m_bookmarkSyncEnabled );
0250         if ( isBookmarkSyncEnabled() ) {
0251             startBookmarkSync();
0252         }
0253     }
0254 }
0255 
0256 void BookmarkSyncManager::setBookmarkManager(BookmarkManager *manager)
0257 {
0258   d->m_bookmarkManager = manager;
0259   connect( manager, SIGNAL(bookmarksChanged()), this, SLOT(startBookmarkSync()) );
0260   startBookmarkSync();
0261 }
0262 
0263 void BookmarkSyncManager::startBookmarkSync()
0264 {
0265     if ( !d->m_cloudSyncManager->isSyncEnabled() || !isBookmarkSyncEnabled() )
0266     {
0267         return;
0268     }
0269 
0270     d->m_syncTimer.start();
0271     d->downloadTimestamp();
0272 }
0273 
0274 QUrl BookmarkSyncManager::Private::endpointUrl( const QString &endpoint ) const
0275 {
0276     return QUrl(m_cloudSyncManager->apiUrl().toString() + QLatin1Char('/') + endpoint);
0277 }
0278 
0279 void BookmarkSyncManager::Private::uploadBookmarks()
0280 {
0281     QByteArray data;
0282     QByteArray lineBreak = "\r\n";
0283     QString word = "----MarbleCloudBoundary";
0284     QString boundary = QString( "--%0" ).arg( word );
0285     QNetworkRequest request( endpointUrl( m_uploadEndpoint ) );
0286     request.setHeader( QNetworkRequest::ContentTypeHeader, QString( "multipart/form-data; boundary=%0" ).arg( word ) );
0287 
0288     data.append( QString( boundary + lineBreak ).toUtf8() );
0289     data.append( "Content-Disposition: form-data; name=\"bookmarks\"; filename=\"bookmarks.kml\"" + lineBreak );
0290     data.append( "Content-Type: application/vnd.google-earth.kml+xml" + lineBreak + lineBreak );
0291 
0292     QFile bookmarksFile( m_localBookmarksPath );
0293     if( !bookmarksFile.open( QFile::ReadOnly ) ) {
0294         mDebug() << "Failed to open file" << bookmarksFile.fileName()
0295                  <<  ". It is either missing or not readable.";
0296         return;
0297     }
0298 
0299     QByteArray kmlContent = bookmarksFile.readAll();
0300     data.append( kmlContent + lineBreak + lineBreak );
0301     data.append( QString( boundary ).toUtf8() );
0302     bookmarksFile.close();
0303 
0304     m_uploadReply = m_network.post( request, data );
0305     connect( m_uploadReply, SIGNAL(uploadProgress(qint64,qint64)),
0306              m_q, SIGNAL(uploadProgress(qint64,qint64)) );
0307     connect( m_uploadReply, SIGNAL(finished()),
0308              m_q, SLOT(completeUpload()) );
0309 }
0310 
0311 void BookmarkSyncManager::Private::downloadBookmarks()
0312 {
0313     QNetworkRequest request( endpointUrl( m_downloadEndpoint ) );
0314     m_downloadReply = m_network.get( request );
0315     connect( m_downloadReply, SIGNAL(finished()),
0316              m_q, SLOT(completeSynchronization()) );
0317     connect( m_downloadReply, SIGNAL(downloadProgress(qint64,qint64)),
0318              m_q, SIGNAL(downloadProgress(qint64,qint64)) );
0319 }
0320 
0321 void BookmarkSyncManager::Private::downloadTimestamp()
0322 {
0323     mDebug() << "Determining remote bookmark state.";
0324     m_timestampReply = m_network.get( QNetworkRequest( endpointUrl( m_timestampEndpoint ) ) );
0325     connect( m_timestampReply, SIGNAL(finished()),
0326              m_q, SLOT(parseTimestamp()) );
0327 }
0328 
0329 bool BookmarkSyncManager::Private::cloudBookmarksModified( const QString &cloudTimestamp ) const
0330 {
0331     QStringList entryList = QDir( m_cachePath ).entryList(
0332                 // TODO: replace with regex filter that only
0333                 // allows timestamp filenames
0334                 QStringList() << "*.kml",
0335                 QDir::NoFilter, QDir::Name );
0336     if( !entryList.isEmpty() ) {
0337         QString lastSynced = entryList.last();
0338         lastSynced.chop( 4 );
0339         return cloudTimestamp != lastSynced;
0340     } else {
0341         return true; // That will let cloud one get downloaded.
0342     }
0343 }
0344 
0345 void BookmarkSyncManager::Private::clearCache()
0346 {
0347     QDir cacheDir( m_cachePath );
0348     QFileInfoList fileInfoList = cacheDir.entryInfoList(
0349                 QStringList() << "*.kml",
0350                 QDir::NoFilter, QDir::Name );
0351     if( !fileInfoList.isEmpty() ) {
0352         for ( const QFileInfo& fileInfo: fileInfoList ) {
0353             QFile file( fileInfo.absoluteFilePath() );
0354             bool removed = file.remove();
0355             if( !removed ) {
0356                 mDebug() << "Could not delete" << file.fileName() <<
0357                          "Make sure you have sufficient permissions.";
0358             }
0359         }
0360     }
0361 }
0362 
0363 QString BookmarkSyncManager::Private::lastSyncedKmlPath() const
0364 {
0365     QDir cacheDir( m_cachePath );
0366     QFileInfoList fileInfoList = cacheDir.entryInfoList(
0367                 QStringList() << "*.kml",
0368                 QDir::NoFilter, QDir::Name );
0369     if( !fileInfoList.isEmpty() ) {
0370         return fileInfoList.last().absoluteFilePath();
0371     } else {
0372         return QString();
0373     }
0374 }
0375 
0376 QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks( GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection )
0377 {
0378     QList<DiffItem> diffItems;
0379     for ( GeoDataFolder *folder: document->folderList() ) {
0380         QString path = QString( "/%0" ).arg( folder->name() );
0381         diffItems.append( getPlacemarks( folder, path, other, diffDirection ) );
0382     }
0383 
0384     return diffItems;
0385 }
0386 
0387 QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks( GeoDataFolder *folder, QString &path, GeoDataDocument *other, DiffItem::Status diffDirection )
0388 {
0389     QList<DiffItem> diffItems;
0390     for ( GeoDataFolder *subFolder: folder->folderList() ) {
0391         QString newPath = QString( "%0/%1" ).arg( path, subFolder->name() );
0392         diffItems.append( getPlacemarks( subFolder, newPath, other, diffDirection ) );
0393     }
0394 
0395     for( GeoDataPlacemark *placemark: folder->placemarkList() ) {
0396         DiffItem diffItem;
0397         diffItem.m_path = path;
0398         diffItem.m_placemarkA = *placemark;
0399         switch ( diffDirection ) {
0400         case DiffItem::Source:
0401             diffItem.m_origin = DiffItem::Destination;
0402             break;
0403         case DiffItem::Destination:
0404             diffItem.m_origin = DiffItem::Source;
0405             break;
0406         default:
0407             break;
0408         }
0409 
0410         determineDiffStatus( diffItem, other );
0411 
0412         if( !( diffItem.m_action == DiffItem::NoAction && diffItem.m_origin == DiffItem::Destination )
0413                 && !( diffItem.m_action == DiffItem::Changed && diffItem.m_origin == DiffItem::Source ) ) {
0414             diffItems.append( diffItem );
0415         }
0416     }
0417 
0418     return diffItems;
0419 }
0420 
0421 const GeoDataPlacemark* BookmarkSyncManager::Private::findPlacemark( GeoDataContainer* container, const GeoDataPlacemark &bookmark ) const
0422 {
0423     for( GeoDataPlacemark* placemark: container->placemarkList() ) {
0424         if (EARTH_RADIUS * placemark->coordinate().sphericalDistanceTo(bookmark.coordinate()) <= 1) {
0425             return placemark;
0426         }
0427     }
0428 
0429     for( GeoDataFolder* folder: container->folderList() ) {
0430         const GeoDataPlacemark* placemark = findPlacemark( folder, bookmark );
0431         if ( placemark ) {
0432             return placemark;
0433         }
0434     }
0435 
0436     return nullptr;
0437 }
0438 
0439 void BookmarkSyncManager::Private::determineDiffStatus( DiffItem &item, GeoDataDocument *document ) const
0440 {
0441     const GeoDataPlacemark *match = findPlacemark( document, item.m_placemarkA );
0442 
0443     if( match != nullptr ) {
0444         item.m_placemarkB = *match;
0445         bool nameChanged = item.m_placemarkA.name() != item.m_placemarkB.name();
0446         bool descChanged = item.m_placemarkA.description() != item.m_placemarkB.description();
0447         bool lookAtChanged = item.m_placemarkA.lookAt()->latitude() != item.m_placemarkB.lookAt()->latitude() ||
0448                 item.m_placemarkA.lookAt()->longitude() != item.m_placemarkB.lookAt()->longitude() ||
0449                 item.m_placemarkA.lookAt()->altitude() != item.m_placemarkB.lookAt()->altitude() ||
0450                 item.m_placemarkA.lookAt()->range() != item.m_placemarkB.lookAt()->range();
0451         if(  nameChanged || descChanged || lookAtChanged ) {
0452             item.m_action = DiffItem::Changed;
0453         } else {
0454             item.m_action = DiffItem::NoAction;
0455         }
0456     } else {
0457         switch( item.m_origin ) {
0458         case DiffItem::Source:
0459             item.m_action = DiffItem::Deleted;
0460             item.m_placemarkB = item.m_placemarkA; // for conflict purposes
0461             break;
0462         case DiffItem::Destination:
0463             item.m_action = DiffItem::Created;
0464             break;
0465         }
0466 
0467     }
0468 }
0469 
0470 QList<DiffItem> BookmarkSyncManager::Private::diff( QString &sourcePath, QString &destinationPath )
0471 {
0472     QFile fileB( destinationPath );
0473     if( !fileB.open( QFile::ReadOnly ) ) {
0474         mDebug() << "Could not open file " << fileB.fileName();
0475     }
0476     return diff( sourcePath, &fileB );
0477 }
0478 
0479 QList<DiffItem> BookmarkSyncManager::Private::diff( QString &sourcePath, QIODevice *fileB )
0480 {
0481     QFile fileA( sourcePath );
0482     if( !fileA.open( QFile::ReadOnly ) ) {
0483         mDebug() << "Could not open file " << fileA.fileName();
0484     }
0485 
0486     return diff( &fileA, fileB );
0487 }
0488 
0489 QList<DiffItem> BookmarkSyncManager::Private::diff( QIODevice *source, QString &destinationPath )
0490 {
0491     QFile fileB( destinationPath );
0492     if( !fileB.open( QFile::ReadOnly ) ) {
0493         mDebug() << "Could not open file " << fileB.fileName();
0494     }
0495 
0496     return diff( source, &fileB );
0497 }
0498 
0499 QList<DiffItem> BookmarkSyncManager::Private::diff( QIODevice *fileA, QIODevice *fileB )
0500 {
0501     GeoDataParser parserA( GeoData_KML );
0502     parserA.read( fileA );
0503     GeoDataDocument *documentA = dynamic_cast<GeoDataDocument*>( parserA.releaseDocument() );
0504 
0505     GeoDataParser parserB( GeoData_KML );
0506     parserB.read( fileB );
0507     GeoDataDocument *documentB = dynamic_cast<GeoDataDocument*>( parserB.releaseDocument() );
0508 
0509     QList<DiffItem> diffItems = getPlacemarks( documentA, documentB, DiffItem::Destination ); // Compare old to new
0510     diffItems.append( getPlacemarks( documentB, documentA, DiffItem::Source ) ); // Compare new to old
0511 
0512     // Compare paths
0513     for( int i = 0; i < diffItems.count(); i++ ) {
0514         for( int p = i + 1; p < diffItems.count(); p++ ) {
0515             if( ( diffItems[i].m_origin == DiffItem::Source )
0516                     && ( diffItems[i].m_action == DiffItem::NoAction )
0517                     && ( EARTH_RADIUS * diffItems[i].m_placemarkA.coordinate().sphericalDistanceTo(diffItems[p].m_placemarkB.coordinate()) <= 1 )
0518                     && ( EARTH_RADIUS * diffItems[i].m_placemarkB.coordinate().sphericalDistanceTo(diffItems[p].m_placemarkA.coordinate()) <= 1 )
0519                     && ( diffItems[i].m_path != diffItems[p].m_path ) ) {
0520                 diffItems[p].m_action = DiffItem::Changed;
0521             }
0522         }
0523     }
0524 
0525     return diffItems;
0526 }
0527 
0528 void BookmarkSyncManager::Private::merge()
0529 {
0530     for( const DiffItem &itemA: m_diffA ) {
0531         if( itemA.m_action == DiffItem::NoAction ) {
0532             bool deleted = false;
0533             bool changed = false;
0534             DiffItem other;
0535 
0536             for( const DiffItem &itemB: m_diffB ) {
0537                 if( EARTH_RADIUS * itemA.m_placemarkA.coordinate().sphericalDistanceTo(itemB.m_placemarkA.coordinate()) <= 1 ) {
0538                     if( itemB.m_action == DiffItem::Deleted ) {
0539                         deleted = true;
0540                     } else if( itemB.m_action == DiffItem::Changed ) {
0541                         changed = true;
0542                         other = itemB;
0543                     }
0544                 }
0545             }
0546             if( changed ) {
0547                 m_merged.append( other );
0548             } else if( !deleted ) {
0549                 m_merged.append( itemA );
0550             }
0551         } else if( itemA.m_action == DiffItem::Created ) {
0552             m_merged.append( itemA );
0553         } else if( itemA.m_action == DiffItem::Changed || itemA.m_action == DiffItem::Deleted ) {
0554             bool conflict = false;
0555             DiffItem other;
0556 
0557             for( const DiffItem &itemB: m_diffB ) {
0558                 if (EARTH_RADIUS * itemA.m_placemarkB.coordinate().sphericalDistanceTo(itemB.m_placemarkB.coordinate()) <= 1) {
0559                     if( ( itemA.m_action == DiffItem::Changed && ( itemB.m_action == DiffItem::Changed || itemB.m_action == DiffItem::Deleted ) )
0560                             || ( itemA.m_action == DiffItem::Deleted && itemB.m_action == DiffItem::Changed ) ) {
0561                         conflict = true;
0562                         other = itemB;
0563                     }
0564                 }
0565             }
0566 
0567             if( !conflict && itemA.m_action == DiffItem::Changed ) {
0568                 m_merged.append( itemA );
0569             } else if ( conflict ) {
0570                 m_conflictItem = other;
0571                 MergeItem *mergeItem = new MergeItem();
0572                 mergeItem->setPathA( itemA.m_path );
0573                 mergeItem->setPathB( other.m_path );
0574                 mergeItem->setPlacemarkA( itemA.m_placemarkA );
0575                 mergeItem->setPlacemarkB( other.m_placemarkA );
0576 
0577                 switch( itemA.m_action ) {
0578                 case DiffItem::Changed:
0579                     mergeItem->setActionA( MergeItem::Changed );
0580                     break;
0581                 case DiffItem::Deleted:
0582                     mergeItem->setActionA( MergeItem::Deleted );
0583                     break;
0584                 default:
0585                     break;
0586                 }
0587 
0588                 switch( other.m_action ) {
0589                 case DiffItem::Changed:
0590                     mergeItem->setActionB( MergeItem::Changed );
0591                     break;
0592                 case DiffItem::Deleted:
0593                     mergeItem->setActionB( MergeItem::Deleted );
0594                     break;
0595                 default:
0596                     break;
0597                 }
0598 
0599                 emit m_q->mergeConflict( mergeItem );
0600                 return;
0601             }
0602         }
0603 
0604         if( !m_diffA.isEmpty() ) {
0605             m_diffA.removeFirst();
0606         }
0607     }
0608 
0609     for( const DiffItem &itemB: m_diffB ) {
0610         if( itemB.m_action == DiffItem::Created ) {
0611             m_merged.append( itemB );
0612         }
0613     }
0614 
0615     completeMerge();
0616 }
0617 
0618 GeoDataFolder* BookmarkSyncManager::Private::createFolders( GeoDataContainer *container, QStringList &pathList )
0619 {
0620     GeoDataFolder *folder = nullptr;
0621     if( pathList.count() > 0 ) {
0622         QString name = pathList.takeFirst();
0623 
0624         for( GeoDataFolder *otherFolder: container->folderList() ) {
0625             if( otherFolder->name() == name ) {
0626                 folder = otherFolder;
0627             }
0628         }
0629 
0630         if( folder == nullptr ) {
0631             folder = new GeoDataFolder();
0632             folder->setName( name );
0633             container->append( folder );
0634         }
0635 
0636         if( pathList.count() == 0 ) {
0637             return folder;
0638         }
0639     }
0640 
0641     return createFolders( folder, pathList );
0642 }
0643 
0644 GeoDataDocument* BookmarkSyncManager::Private::constructDocument( const QList<DiffItem> &mergedList )
0645 {
0646     GeoDataDocument *document = new GeoDataDocument();
0647     document->setName( tr( "Bookmarks" ) );
0648 
0649     for( const DiffItem &item: mergedList ) {
0650         GeoDataPlacemark *placemark = new GeoDataPlacemark( item.m_placemarkA );
0651         QStringList splitten = item.m_path.split(QLatin1Char('/'), QString::SkipEmptyParts);
0652         GeoDataFolder *folder = createFolders( document, splitten );
0653         folder->append( placemark );
0654     }
0655 
0656     return document;
0657 }
0658 
0659 void BookmarkSyncManager::resolveConflict( MergeItem *item )
0660 {
0661     DiffItem diffItem;
0662 
0663     switch( item->resolution() ) {
0664     case MergeItem::A:
0665         if( !d->m_diffA.isEmpty() ) {
0666             diffItem = d->m_diffA.first();
0667             break;
0668         }
0669         Q_FALLTHROUGH();
0670     case MergeItem::B:
0671         diffItem = d->m_conflictItem;
0672         break;
0673     default:
0674         return; // It shouldn't happen.
0675     }
0676 
0677     if( diffItem.m_action != DiffItem::Deleted ) {
0678         d->m_merged.append( diffItem );
0679     }
0680 
0681     if( !d->m_diffA.isEmpty() ) {
0682         d->m_diffA.removeFirst();
0683     }
0684 
0685     d->merge();
0686 }
0687 
0688 void BookmarkSyncManager::Private::saveDownloadedToCache( const QByteArray &kml )
0689 {
0690     QString localBookmarksDir = m_localBookmarksPath;
0691     QDir().mkdir( localBookmarksDir.remove( "bookmarks.kml" ) );
0692     QFile bookmarksFile( m_localBookmarksPath );
0693     if( !bookmarksFile.open( QFile::ReadWrite ) ) {
0694         mDebug() << "Failed to open file" << bookmarksFile.fileName()
0695                  <<  ". It is either missing or not readable.";
0696         return;
0697     }
0698 
0699     bookmarksFile.write( kml );
0700     bookmarksFile.close();
0701     copyLocalToCache();
0702 }
0703 
0704 void BookmarkSyncManager::Private::parseTimestamp()
0705 {
0706     QJsonDocument jsonDoc = QJsonDocument::fromJson(m_timestampReply->readAll());
0707     QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data"));
0708 
0709     m_cloudTimestamp = dataValue.toString();
0710     mDebug() << "Remote bookmark timestamp is " << m_cloudTimestamp;
0711     continueSynchronization();
0712 }
0713 void BookmarkSyncManager::Private::copyLocalToCache()
0714 {
0715     QDir().mkpath( m_cachePath );
0716     clearCache();
0717 
0718     QFile bookmarksFile( m_localBookmarksPath );
0719     bookmarksFile.copy( QString( "%0/%1.kml" ).arg( m_cachePath, m_cloudTimestamp ) );
0720 }
0721 
0722 // Bookmark synchronization steps
0723 void BookmarkSyncManager::Private::continueSynchronization()
0724 {
0725     bool cloudModified = cloudBookmarksModified( m_cloudTimestamp );
0726     if( cloudModified ) {
0727         downloadBookmarks();
0728     } else {
0729         QString lastSyncedPath = lastSyncedKmlPath();
0730         if( lastSyncedPath.isEmpty() ) {
0731             mDebug() << "Never synced. Uploading bookmarks.";
0732             uploadBookmarks();
0733         } else {
0734             QList<DiffItem> diffList = diff( lastSyncedPath, m_localBookmarksPath );
0735             bool localModified = false;
0736             for( const DiffItem &item: diffList ) {
0737                 if( item.m_action != DiffItem::NoAction ) {
0738                     localModified = true;
0739                 }
0740             }
0741 
0742             if( localModified ) {
0743                 mDebug() << "Local modifications, uploading.";
0744                 uploadBookmarks();
0745             }
0746         }
0747     }
0748 }
0749 
0750 void BookmarkSyncManager::Private::completeSynchronization()
0751 {
0752     mDebug() << "Merging remote and local bookmark file";
0753     QString lastSyncedPath = lastSyncedKmlPath();
0754     QFile localBookmarksFile( m_localBookmarksPath );
0755     QByteArray result = m_downloadReply->readAll();
0756     QBuffer buffer( &result );
0757     buffer.open( QIODevice::ReadOnly );
0758 
0759     if( lastSyncedPath.isEmpty() ) {
0760         if( localBookmarksFile.exists() ) {
0761             mDebug() << "Conflict between remote bookmarks and local ones";
0762             m_diffA = diff( &buffer, m_localBookmarksPath );
0763             m_diffB = diff( m_localBookmarksPath, &buffer );
0764         } else {
0765             saveDownloadedToCache( result );
0766             return;
0767         }
0768     }
0769     else
0770     {
0771         m_diffA = diff( lastSyncedPath, m_localBookmarksPath );
0772         m_diffB = diff( lastSyncedPath, &buffer );
0773     }
0774 
0775     m_merged.clear();
0776     merge();
0777 }
0778 
0779 void BookmarkSyncManager::Private::completeMerge()
0780 {
0781     QFile localBookmarksFile( m_localBookmarksPath );
0782     GeoDataDocument *doc = constructDocument( m_merged );
0783     GeoWriter writer;
0784     localBookmarksFile.remove();
0785     localBookmarksFile.open( QFile::ReadWrite );
0786     writer.write( &localBookmarksFile, doc );
0787     localBookmarksFile.close();
0788     uploadBookmarks();
0789 }
0790 
0791 void BookmarkSyncManager::Private::completeUpload()
0792 {
0793     QJsonDocument jsonDoc = QJsonDocument::fromJson(m_uploadReply->readAll());
0794     QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data"));
0795 
0796     m_cloudTimestamp = dataValue.toString();
0797     mDebug() << "Uploaded bookmarks to remote server. Timestamp is " << m_cloudTimestamp;
0798     copyLocalToCache();
0799     emit m_q->syncComplete();
0800 }
0801 
0802 }
0803 
0804 #include "moc_BookmarkSyncManager.cpp"