6 #include "BookmarkSyncManager.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"
19 #include "BookmarkManager.h"
23 #include <QJsonDocument>
24 #include <QJsonObject>
25 #include <QNetworkAccessManager>
26 #include <QNetworkReply>
49 GeoDataPlacemark m_placemarkA;
50 GeoDataPlacemark m_placemarkB;
53 class Q_DECL_HIDDEN BookmarkSyncManager::Private
56 Private( BookmarkSyncManager* parent, CloudSyncManager *cloudSyncManager );
58 BookmarkSyncManager* m_q;
59 CloudSyncManager *m_cloudSyncManager;
79 DiffItem m_conflictItem;
81 BookmarkManager* m_bookmarkManager;
83 bool m_bookmarkSyncEnabled;
95 void uploadBookmarks();
100 void downloadBookmarks();
105 void downloadTimestamp();
111 bool cloudBookmarksModified(
const QString &cloudTimestamp )
const;
123 QString lastSyncedKmlPath()
const;
132 QList<DiffItem> getPlacemarks(GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection );
142 QList<DiffItem> getPlacemarks( GeoDataFolder *folder,
QString &path, GeoDataDocument *other, DiffItem::Status diffDirection );
150 const GeoDataPlacemark* findPlacemark( GeoDataContainer* container,
const GeoDataPlacemark &bookmark )
const;
158 void determineDiffStatus( DiffItem &item, GeoDataDocument* document )
const;
185 GeoDataFolder* createFolders( GeoDataContainer *container,
QStringList &pathList );
192 GeoDataDocument* constructDocument(
const QList<DiffItem> &mergedList );
194 void saveDownloadedToCache(
const QByteArray &kml );
196 void parseTimestamp();
197 void copyLocalToCache();
199 void continueSynchronization();
200 void completeSynchronization();
201 void completeMerge();
202 void completeUpload();
205 BookmarkSyncManager::Private::Private(BookmarkSyncManager *parent, CloudSyncManager *cloudSyncManager ) :
207 m_cloudSyncManager( cloudSyncManager ),
208 m_bookmarkManager( nullptr ),
209 m_bookmarkSyncEnabled( false )
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";
218 BookmarkSyncManager::BookmarkSyncManager( CloudSyncManager *cloudSyncManager ) :
220 d( new Private( this, cloudSyncManager ) )
222 d->m_syncTimer.setInterval( 60 * 60 * 1000 );
223 connect( &d->m_syncTimer, SIGNAL(timeout()),
this, SLOT(startBookmarkSync()) );
226 BookmarkSyncManager::~BookmarkSyncManager()
231 QDateTime BookmarkSyncManager::lastSync()
const
233 const QString last = d->lastSyncedKmlPath();
239 bool BookmarkSyncManager::isBookmarkSyncEnabled()
const
241 return d->m_bookmarkSyncEnabled;
244 void BookmarkSyncManager::setBookmarkSyncEnabled(
bool enabled )
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() ) {
256 void BookmarkSyncManager::setBookmarkManager(BookmarkManager *manager)
258 d->m_bookmarkManager = manager;
259 connect( manager, SIGNAL(bookmarksChanged()),
this, SLOT(startBookmarkSync()) );
263 void BookmarkSyncManager::startBookmarkSync()
265 if ( !d->m_cloudSyncManager->isSyncEnabled() || !isBookmarkSyncEnabled() )
270 d->m_syncTimer.start();
271 d->downloadTimestamp();
274 QUrl BookmarkSyncManager::Private::endpointUrl(
const QString &endpoint )
const
276 return QUrl(m_cloudSyncManager->apiUrl().toString() +
QLatin1Char(
'/') + endpoint);
279 void BookmarkSyncManager::Private::uploadBookmarks()
283 QString word =
"----MarbleCloudBoundary";
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 );
292 QFile bookmarksFile( m_localBookmarksPath );
294 mDebug() <<
"Failed to open file" << bookmarksFile.fileName()
295 <<
". It is either missing or not readable.";
299 QByteArray kmlContent = bookmarksFile.readAll();
300 data.
append( kmlContent + lineBreak + lineBreak );
302 bookmarksFile.close();
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()) );
311 void BookmarkSyncManager::Private::downloadBookmarks()
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)) );
321 void BookmarkSyncManager::Private::downloadTimestamp()
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()) );
329 bool BookmarkSyncManager::Private::cloudBookmarksModified(
const QString &cloudTimestamp )
const
338 lastSynced.
chop( 4 );
339 return cloudTimestamp != lastSynced;
345 void BookmarkSyncManager::Private::clearCache()
347 QDir cacheDir( m_cachePath );
348 QFileInfoList fileInfoList = cacheDir.entryInfoList(
351 if( !fileInfoList.isEmpty() ) {
352 for (
const QFileInfo& fileInfo: fileInfoList ) {
353 QFile file( fileInfo.absoluteFilePath() );
354 bool removed = file.remove();
356 mDebug() <<
"Could not delete" << file.fileName() <<
357 "Make sure you have sufficient permissions.";
363 QString BookmarkSyncManager::Private::lastSyncedKmlPath()
const
365 QDir cacheDir( m_cachePath );
366 QFileInfoList fileInfoList = cacheDir.entryInfoList(
369 if( !fileInfoList.isEmpty() ) {
370 return fileInfoList.last().absoluteFilePath();
376 QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks( GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection )
379 for ( GeoDataFolder *folder: document->folderList() ) {
381 diffItems.
append( getPlacemarks( folder, path, other, diffDirection ) );
387 QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks( GeoDataFolder *folder,
QString &path, GeoDataDocument *other, DiffItem::Status diffDirection )
390 for ( GeoDataFolder *subFolder: folder->folderList() ) {
392 diffItems.
append( getPlacemarks( subFolder, newPath, other, diffDirection ) );
395 for( GeoDataPlacemark *placemark: folder->placemarkList() ) {
397 diffItem.m_path =
path;
398 diffItem.m_placemarkA = *placemark;
399 switch ( diffDirection ) {
400 case DiffItem::Source:
401 diffItem.m_origin = DiffItem::Destination;
403 case DiffItem::Destination:
404 diffItem.m_origin = DiffItem::Source;
410 determineDiffStatus( diffItem, other );
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 );
421 const GeoDataPlacemark* BookmarkSyncManager::Private::findPlacemark( GeoDataContainer* container,
const GeoDataPlacemark &bookmark )
const
423 for( GeoDataPlacemark* placemark: container->placemarkList() ) {
424 if (EARTH_RADIUS * placemark->coordinate().sphericalDistanceTo(bookmark.coordinate()) <= 1) {
429 for( GeoDataFolder* folder: container->folderList() ) {
430 const GeoDataPlacemark* placemark = findPlacemark( folder, bookmark );
439 void BookmarkSyncManager::Private::determineDiffStatus( DiffItem &item, GeoDataDocument *document )
const
441 const GeoDataPlacemark *
match = findPlacemark( document, item.m_placemarkA );
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;
454 item.m_action = DiffItem::NoAction;
457 switch( item.m_origin ) {
458 case DiffItem::Source:
459 item.m_action = DiffItem::Deleted;
460 item.m_placemarkB = item.m_placemarkA;
462 case DiffItem::Destination:
463 item.m_action = DiffItem::Created;
472 QFile fileB( destinationPath );
474 mDebug() <<
"Could not open file " << fileB.fileName();
476 return diff( sourcePath, &fileB );
481 QFile fileA( sourcePath );
483 mDebug() <<
"Could not open file " << fileA.fileName();
486 return diff( &fileA, fileB );
491 QFile fileB( destinationPath );
493 mDebug() <<
"Could not open file " << fileB.fileName();
496 return diff( source, &fileB );
501 GeoDataParser parserA( GeoData_KML );
502 parserA.read( fileA );
503 GeoDataDocument *documentA =
dynamic_cast<GeoDataDocument*
>( parserA.releaseDocument() );
505 GeoDataParser parserB( GeoData_KML );
506 parserB.read( fileB );
507 GeoDataDocument *documentB =
dynamic_cast<GeoDataDocument*
>( parserB.releaseDocument() );
509 QList<DiffItem> diffItems = getPlacemarks( documentA, documentB, DiffItem::Destination );
510 diffItems.
append( getPlacemarks( documentB, documentA, DiffItem::Source ) );
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;
528 void BookmarkSyncManager::Private::merge()
530 for(
const DiffItem &itemA: m_diffA ) {
531 if( itemA.m_action == DiffItem::NoAction ) {
532 bool deleted =
false;
533 bool changed =
false;
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 ) {
540 }
else if( itemB.m_action == DiffItem::Changed ) {
547 m_merged.append( other );
548 }
else if( !deleted ) {
549 m_merged.append( itemA );
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;
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 ) ) {
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 );
577 switch( itemA.m_action ) {
578 case DiffItem::Changed:
579 mergeItem->setActionA( MergeItem::Changed );
581 case DiffItem::Deleted:
582 mergeItem->setActionA( MergeItem::Deleted );
588 switch( other.m_action ) {
589 case DiffItem::Changed:
590 mergeItem->setActionB( MergeItem::Changed );
592 case DiffItem::Deleted:
593 mergeItem->setActionB( MergeItem::Deleted );
599 emit m_q->mergeConflict( mergeItem );
604 if( !m_diffA.isEmpty() ) {
605 m_diffA.removeFirst();
609 for(
const DiffItem &itemB: m_diffB ) {
610 if( itemB.m_action == DiffItem::Created ) {
611 m_merged.append( itemB );
618 GeoDataFolder* BookmarkSyncManager::Private::createFolders( GeoDataContainer *container,
QStringList &pathList )
620 GeoDataFolder *folder =
nullptr;
621 if( pathList.
count() > 0 ) {
624 for( GeoDataFolder *otherFolder: container->folderList() ) {
625 if( otherFolder->name() == name ) {
626 folder = otherFolder;
630 if( folder ==
nullptr ) {
631 folder =
new GeoDataFolder();
632 folder->setName( name );
633 container->append( folder );
636 if( pathList.
count() == 0 ) {
641 return createFolders( folder, pathList );
644 GeoDataDocument* BookmarkSyncManager::Private::constructDocument(
const QList<DiffItem> &mergedList )
646 GeoDataDocument *document =
new GeoDataDocument();
647 document->setName( tr(
"Bookmarks" ) );
649 for(
const DiffItem &item: mergedList ) {
650 GeoDataPlacemark *placemark =
new GeoDataPlacemark( item.m_placemarkA );
652 GeoDataFolder *folder = createFolders( document, splitten );
653 folder->append( placemark );
659 void BookmarkSyncManager::resolveConflict( MergeItem *item )
663 switch( item->resolution() ) {
665 if( !d->m_diffA.isEmpty() ) {
666 diffItem = d->m_diffA.first();
671 diffItem = d->m_conflictItem;
677 if( diffItem.m_action != DiffItem::Deleted ) {
678 d->m_merged.append( diffItem );
681 if( !d->m_diffA.isEmpty() ) {
682 d->m_diffA.removeFirst();
688 void BookmarkSyncManager::Private::saveDownloadedToCache(
const QByteArray &kml )
690 QString localBookmarksDir = m_localBookmarksPath;
692 QFile bookmarksFile( m_localBookmarksPath );
694 mDebug() <<
"Failed to open file" << bookmarksFile.fileName()
695 <<
". It is either missing or not readable.";
699 bookmarksFile.write( kml );
700 bookmarksFile.close();
704 void BookmarkSyncManager::Private::parseTimestamp()
709 m_cloudTimestamp = dataValue.
toString();
710 mDebug() <<
"Remote bookmark timestamp is " << m_cloudTimestamp;
711 continueSynchronization();
713 void BookmarkSyncManager::Private::copyLocalToCache()
718 QFile bookmarksFile( m_localBookmarksPath );
719 bookmarksFile.copy(
QString(
"%0/%1.kml" ).arg( m_cachePath, m_cloudTimestamp ) );
723 void BookmarkSyncManager::Private::continueSynchronization()
725 bool cloudModified = cloudBookmarksModified( m_cloudTimestamp );
726 if( cloudModified ) {
729 QString lastSyncedPath = lastSyncedKmlPath();
730 if( lastSyncedPath.
isEmpty() ) {
731 mDebug() <<
"Never synced. Uploading bookmarks.";
734 QList<DiffItem> diffList = diff( lastSyncedPath, m_localBookmarksPath );
735 bool localModified =
false;
736 for(
const DiffItem &item: diffList ) {
737 if( item.m_action != DiffItem::NoAction ) {
738 localModified =
true;
742 if( localModified ) {
743 mDebug() <<
"Local modifications, uploading.";
750 void BookmarkSyncManager::Private::completeSynchronization()
752 mDebug() <<
"Merging remote and local bookmark file";
753 QString lastSyncedPath = lastSyncedKmlPath();
754 QFile localBookmarksFile( m_localBookmarksPath );
755 QByteArray result = m_downloadReply->readAll();
759 if( lastSyncedPath.
isEmpty() ) {
760 if( localBookmarksFile.exists() ) {
761 mDebug() <<
"Conflict between remote bookmarks and local ones";
762 m_diffA = diff( &buffer, m_localBookmarksPath );
763 m_diffB = diff( m_localBookmarksPath, &buffer );
765 saveDownloadedToCache( result );
771 m_diffA = diff( lastSyncedPath, m_localBookmarksPath );
772 m_diffB = diff( lastSyncedPath, &buffer );
779 void BookmarkSyncManager::Private::completeMerge()
781 QFile localBookmarksFile( m_localBookmarksPath );
782 GeoDataDocument *doc = constructDocument( m_merged );
784 localBookmarksFile.remove();
786 writer.write( &localBookmarksFile, doc );
787 localBookmarksFile.close();
791 void BookmarkSyncManager::Private::completeUpload()
796 m_cloudTimestamp = dataValue.
toString();
797 mDebug() <<
"Uploaded bookmarks to remote server. Timestamp is " << m_cloudTimestamp;
799 emit m_q->syncComplete();
804 #include "moc_BookmarkSyncManager.cpp"