6#include "BookmarkSyncManager.h" 
    8#include "BookmarkManager.h" 
    9#include "CloudSyncManager.h" 
   10#include "GeoDataCoordinates.h" 
   11#include "GeoDataDocument.h" 
   12#include "GeoDataFolder.h" 
   13#include "GeoDataLookAt.h" 
   14#include "GeoDataParser.h" 
   16#include "MarbleDebug.h" 
   17#include "MarbleDirs.h" 
   19#include "OwncloudSyncBackend.h" 
   23#include <QJsonDocument> 
   25#include <QNetworkAccessManager> 
   26#include <QNetworkReply> 
   50    GeoDataPlacemark m_placemarkA;
 
   51    GeoDataPlacemark m_placemarkB;
 
   54class Q_DECL_HIDDEN BookmarkSyncManager::Private
 
   57    Private(BookmarkSyncManager *parent, CloudSyncManager *cloudSyncManager);
 
   59    BookmarkSyncManager *
const m_q;
 
   60    CloudSyncManager *
const m_cloudSyncManager;
 
   62    QNetworkAccessManager m_network;
 
   63    QString m_uploadEndpoint;
 
   64    QString m_downloadEndpoint;
 
   65    QString m_timestampEndpoint;
 
   67    QNetworkReply *m_uploadReply;
 
   68    QNetworkReply *m_downloadReply;
 
   69    QNetworkReply *m_timestampReply;
 
   71    QString m_cloudTimestamp;
 
   74    QString m_localBookmarksPath;
 
   75    QString m_bookmarksTimestamp;
 
   77    QList<DiffItem> m_diffA;
 
   78    QList<DiffItem> m_diffB;
 
   79    QList<DiffItem> m_merged;
 
   80    DiffItem m_conflictItem;
 
   82    BookmarkManager *m_bookmarkManager = 
nullptr;
 
   84    bool m_bookmarkSyncEnabled;
 
   91    QUrl endpointUrl(
const QString &endpoint) 
const;
 
   96    void uploadBookmarks();
 
  101    void downloadBookmarks();
 
  106    void downloadTimestamp();
 
  112    bool cloudBookmarksModified(
const QString &cloudTimestamp) 
const;
 
  124    QString lastSyncedKmlPath() 
const;
 
  133    QList<DiffItem> getPlacemarks(GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection);
 
  143    QList<DiffItem> getPlacemarks(GeoDataFolder *folder, QString &path, GeoDataDocument *other, DiffItem::Status diffDirection);
 
  151    const GeoDataPlacemark *findPlacemark(GeoDataContainer *container, 
const GeoDataPlacemark &bookmark) 
const;
 
  159    void determineDiffStatus(DiffItem &item, GeoDataDocument *document) 
const;
 
  167    QList<DiffItem> diff(QString &sourcePath, QString &destinationPath);
 
  168    QList<DiffItem> diff(QString &sourcePath, QIODevice *destination);
 
  169    QList<DiffItem> diff(QIODevice *source, QString &destinationPath);
 
  170    QList<DiffItem> diff(QIODevice *source, QIODevice *destination);
 
  186    GeoDataFolder *createFolders(GeoDataContainer *container, QStringList &pathList);
 
  193    GeoDataDocument *constructDocument(
const QList<DiffItem> &mergedList);
 
  195    void saveDownloadedToCache(
const QByteArray &kml);
 
  197    void parseTimestamp();
 
  198    void copyLocalToCache();
 
  200    void continueSynchronization();
 
  201    void completeSynchronization();
 
  202    void completeMerge();
 
  203    void completeUpload();
 
  206BookmarkSyncManager::Private::Private(BookmarkSyncManager *parent, CloudSyncManager *cloudSyncManager)
 
  208    , m_cloudSyncManager(cloudSyncManager)
 
  209    , m_bookmarkManager(nullptr)
 
  210    , m_bookmarkSyncEnabled(false)
 
  212    m_cachePath = MarbleDirs::localPath() + 
QLatin1StringView(
"/cloudsync/cache/bookmarks");
 
  213    m_localBookmarksPath = MarbleDirs::localPath() + 
QLatin1StringView(
"/bookmarks/bookmarks.kml");
 
  214    m_downloadEndpoint = QStringLiteral(
"bookmarks/kml");
 
  215    m_uploadEndpoint = QStringLiteral(
"bookmarks/update");
 
  216    m_timestampEndpoint = QStringLiteral(
"bookmarks/timestamp");
 
  219BookmarkSyncManager::BookmarkSyncManager(CloudSyncManager *cloudSyncManager)
 
  221    , d(new Private(this, cloudSyncManager))
 
  223    d->m_syncTimer.setInterval(60 * 60 * 1000); 
 
  227BookmarkSyncManager::~BookmarkSyncManager()
 
  232QDateTime BookmarkSyncManager::lastSync()
 const 
  234    const QString last = d->lastSyncedKmlPath();
 
  240bool BookmarkSyncManager::isBookmarkSyncEnabled()
 const 
  242    return d->m_bookmarkSyncEnabled;
 
  245void BookmarkSyncManager::setBookmarkSyncEnabled(
bool enabled)
 
  247    bool const old_state = isBookmarkSyncEnabled();
 
  248    d->m_bookmarkSyncEnabled = enabled;
 
  249    if (old_state != isBookmarkSyncEnabled()) {
 
  250        Q_EMIT bookmarkSyncEnabledChanged(d->m_bookmarkSyncEnabled);
 
  251        if (isBookmarkSyncEnabled()) {
 
  257void BookmarkSyncManager::setBookmarkManager(BookmarkManager *manager)
 
  259    d->m_bookmarkManager = manager;
 
  260    connect(manager, SIGNAL(bookmarksChanged()), 
this, SLOT(startBookmarkSync()));
 
  264void BookmarkSyncManager::startBookmarkSync()
 
  266    if (!d->m_cloudSyncManager->isSyncEnabled() || !isBookmarkSyncEnabled()) {
 
  270    d->m_syncTimer.start();
 
  271    d->downloadTimestamp();
 
  274QUrl BookmarkSyncManager::Private::endpointUrl(
const QString &endpoint)
 const 
  276    return QUrl(m_cloudSyncManager->apiUrl().toString() + 
QLatin1Char(
'/') + endpoint);
 
  279void BookmarkSyncManager::Private::uploadBookmarks()
 
  283    QString word = QStringLiteral(
"----MarbleCloudBoundary");
 
  284    QString boundary = QStringLiteral(
"--%0").
arg(word);
 
  289    data.
append(R
"(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() << 
". It is either missing or not readable.";
 
  298    QByteArray kmlContent = bookmarksFile.readAll();
 
  299    data.
append(kmlContent + lineBreak + lineBreak);
 
  301    bookmarksFile.close();
 
  303    m_uploadReply = m_network.post(request, data);
 
  304    connect(m_uploadReply, SIGNAL(uploadProgress(qint64, qint64)), m_q, SIGNAL(uploadProgress(qint64, qint64)));
 
  305    connect(m_uploadReply, SIGNAL(finished()), m_q, SLOT(completeUpload()));
 
  308void BookmarkSyncManager::Private::downloadBookmarks()
 
  311    m_downloadReply = m_network.get(request);
 
  312    connect(m_downloadReply, SIGNAL(finished()), m_q, SLOT(completeSynchronization()));
 
  313    connect(m_downloadReply, SIGNAL(downloadProgress(qint64, qint64)), m_q, SIGNAL(downloadProgress(qint64, qint64)));
 
  316void BookmarkSyncManager::Private::downloadTimestamp()
 
  318    mDebug() << 
"Determining remote bookmark state.";
 
  319    m_timestampReply = m_network.get(
QNetworkRequest(endpointUrl(m_timestampEndpoint)));
 
  320    connect(m_timestampReply, SIGNAL(finished()), m_q, SLOT(parseTimestamp()));
 
  323bool BookmarkSyncManager::Private::cloudBookmarksModified(
const QString &cloudTimestamp)
 const 
  335        return cloudTimestamp != lastSynced;
 
  341void BookmarkSyncManager::Private::clearCache()
 
  343    QDir cacheDir(m_cachePath);
 
  345    if (!fileInfoList.isEmpty()) {
 
  346        for (
const QFileInfo &fileInfo : std::as_const(fileInfoList)) {
 
  347            QFile file(fileInfo.absoluteFilePath());
 
  348            bool removed = file.remove();
 
  350                mDebug() << 
"Could not delete" << file.fileName() << 
"Make sure you have sufficient permissions.";
 
  356QString BookmarkSyncManager::Private::lastSyncedKmlPath()
 const 
  358    QDir cacheDir(m_cachePath);
 
  360    if (!fileInfoList.isEmpty()) {
 
  361        return fileInfoList.last().absoluteFilePath();
 
  367QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks(GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection)
 
  370    const auto folders = document->folderList();
 
  371    for (GeoDataFolder *folder : folders) {
 
  373        diffItems.
append(getPlacemarks(folder, path, other, diffDirection));
 
  379QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks(GeoDataFolder *folder, 
QString &path, GeoDataDocument *other, DiffItem::Status diffDirection)
 
  382    const auto folders = folder->folderList();
 
  383    for (GeoDataFolder *subFolder : folders) {
 
  384        QString newPath = QStringLiteral(
"%0/%1").
arg(path, subFolder->name());
 
  385        diffItems.
append(getPlacemarks(subFolder, newPath, other, diffDirection));
 
  388    const auto placemarks = folder->placemarkList();
 
  389    for (GeoDataPlacemark *placemark : placemarks) {
 
  391        diffItem.m_path = 
path;
 
  392        diffItem.m_placemarkA = *placemark;
 
  393        switch (diffDirection) {
 
  394        case DiffItem::Source:
 
  395            diffItem.m_origin = DiffItem::Destination;
 
  397        case DiffItem::Destination:
 
  398            diffItem.m_origin = DiffItem::Source;
 
  404        determineDiffStatus(diffItem, other);
 
  406        if (!(diffItem.m_action == DiffItem::NoAction && diffItem.m_origin == DiffItem::Destination)
 
  407            && !(diffItem.m_action == DiffItem::Changed && diffItem.m_origin == DiffItem::Source)) {
 
  408            diffItems.
append(diffItem);
 
  415const GeoDataPlacemark *BookmarkSyncManager::Private::findPlacemark(GeoDataContainer *container, 
const GeoDataPlacemark &bookmark)
 const 
  417    const auto placemarks = container->placemarkList();
 
  418    for (GeoDataPlacemark *placemark : placemarks) {
 
  419        if (EARTH_RADIUS * placemark->coordinate().sphericalDistanceTo(bookmark.coordinate()) <= 1) {
 
  424    const auto folders = container->folderList();
 
  425    for (GeoDataFolder *folder : folders) {
 
  426        const GeoDataPlacemark *placemark = findPlacemark(folder, bookmark);
 
  435void BookmarkSyncManager::Private::determineDiffStatus(DiffItem &item, GeoDataDocument *document)
 const 
  437    const GeoDataPlacemark *
match = findPlacemark(document, item.m_placemarkA);
 
  439    if (match != 
nullptr) {
 
  440        item.m_placemarkB = *
match;
 
  441        bool nameChanged = item.m_placemarkA.name() != item.m_placemarkB.name();
 
  442        bool descChanged = item.m_placemarkA.description() != item.m_placemarkB.description();
 
  443        bool lookAtChanged = item.m_placemarkA.lookAt()->latitude() != item.m_placemarkB.lookAt()->latitude()
 
  444            || item.m_placemarkA.lookAt()->longitude() != item.m_placemarkB.lookAt()->longitude()
 
  445            || item.m_placemarkA.lookAt()->altitude() != item.m_placemarkB.lookAt()->altitude()
 
  446            || item.m_placemarkA.lookAt()->range() != item.m_placemarkB.lookAt()->range();
 
  447        if (nameChanged || descChanged || lookAtChanged) {
 
  448            item.m_action = DiffItem::Changed;
 
  450            item.m_action = DiffItem::NoAction;
 
  453        switch (item.m_origin) {
 
  454        case DiffItem::Source:
 
  455            item.m_action = DiffItem::Deleted;
 
  456            item.m_placemarkB = item.m_placemarkA; 
 
  458        case DiffItem::Destination:
 
  459            item.m_action = DiffItem::Created;
 
  467    QFile fileB(destinationPath);
 
  469        mDebug() << 
"Could not open file " << fileB.fileName();
 
  471    return diff(sourcePath, &fileB);
 
  476    QFile fileA(sourcePath);
 
  478        mDebug() << 
"Could not open file " << fileA.fileName();
 
  481    return diff(&fileA, fileB);
 
  486    QFile fileB(destinationPath);
 
  488        mDebug() << 
"Could not open file " << fileB.fileName();
 
  491    return diff(source, &fileB);
 
  496    GeoDataParser parserA(GeoData_KML);
 
  498    auto documentA = 
dynamic_cast<GeoDataDocument *
>(parserA.releaseDocument());
 
  500    GeoDataParser parserB(GeoData_KML);
 
  502    auto documentB = 
dynamic_cast<GeoDataDocument *
>(parserB.releaseDocument());
 
  504    QList<DiffItem> diffItems = getPlacemarks(documentA, documentB, DiffItem::Destination); 
 
  505    diffItems.
append(getPlacemarks(documentB, documentA, DiffItem::Source)); 
 
  508    for (
int i = 0; i < diffItems.
count(); i++) {
 
  509        for (
int p = i + 1; p < diffItems.
count(); p++) {
 
  510            if ((diffItems[i].m_origin == DiffItem::Source) && (diffItems[i].m_action == DiffItem::NoAction)
 
  511                && (EARTH_RADIUS * diffItems[i].m_placemarkA.coordinate().sphericalDistanceTo(diffItems[p].m_placemarkB.coordinate()) <= 1)
 
  512                && (EARTH_RADIUS * diffItems[i].m_placemarkB.coordinate().sphericalDistanceTo(diffItems[p].m_placemarkA.coordinate()) <= 1)
 
  513                && (diffItems[i].m_path != diffItems[p].m_path)) {
 
  514                diffItems[p].m_action = DiffItem::Changed;
 
  522void BookmarkSyncManager::Private::merge()
 
  524    for (
const DiffItem &itemA : std::as_const(m_diffA)) {
 
  525        if (itemA.m_action == DiffItem::NoAction) {
 
  526            bool deleted = 
false;
 
  527            bool changed = 
false;
 
  530            for (
const DiffItem &itemB : std::as_const(m_diffB)) {
 
  531                if (EARTH_RADIUS * itemA.m_placemarkA.coordinate().sphericalDistanceTo(itemB.m_placemarkA.coordinate()) <= 1) {
 
  532                    if (itemB.m_action == DiffItem::Deleted) {
 
  534                    } 
else if (itemB.m_action == DiffItem::Changed) {
 
  541                m_merged.append(other);
 
  542            } 
else if (!deleted) {
 
  543                m_merged.append(itemA);
 
  545        } 
else if (itemA.m_action == DiffItem::Created) {
 
  546            m_merged.append(itemA);
 
  547        } 
else if (itemA.m_action == DiffItem::Changed || itemA.m_action == DiffItem::Deleted) {
 
  548            bool conflict = 
false;
 
  551            for (
const DiffItem &itemB : std::as_const(m_diffB)) {
 
  552                if (EARTH_RADIUS * itemA.m_placemarkB.coordinate().sphericalDistanceTo(itemB.m_placemarkB.coordinate()) <= 1) {
 
  553                    if ((itemA.m_action == DiffItem::Changed && (itemB.m_action == DiffItem::Changed || itemB.m_action == DiffItem::Deleted))
 
  554                        || (itemA.m_action == DiffItem::Deleted && itemB.m_action == DiffItem::Changed)) {
 
  561            if (!conflict && itemA.m_action == DiffItem::Changed) {
 
  562                m_merged.append(itemA);
 
  563            } 
else if (conflict) {
 
  564                m_conflictItem = other;
 
  565                auto mergeItem = 
new MergeItem();
 
  566                mergeItem->setPathA(itemA.m_path);
 
  567                mergeItem->setPathB(other.m_path);
 
  568                mergeItem->setPlacemarkA(itemA.m_placemarkA);
 
  569                mergeItem->setPlacemarkB(other.m_placemarkA);
 
  571                switch (itemA.m_action) {
 
  572                case DiffItem::Changed:
 
  573                    mergeItem->setActionA(MergeItem::Changed);
 
  575                case DiffItem::Deleted:
 
  576                    mergeItem->setActionA(MergeItem::Deleted);
 
  582                switch (other.m_action) {
 
  583                case DiffItem::Changed:
 
  584                    mergeItem->setActionB(MergeItem::Changed);
 
  586                case DiffItem::Deleted:
 
  587                    mergeItem->setActionB(MergeItem::Deleted);
 
  593                Q_EMIT m_q->mergeConflict(mergeItem);
 
  598        if (!m_diffA.isEmpty()) {
 
  599            m_diffA.removeFirst();
 
  603    for (
const DiffItem &itemB : std::as_const(m_diffB)) {
 
  604        if (itemB.m_action == DiffItem::Created) {
 
  605            m_merged.append(itemB);
 
  612GeoDataFolder *BookmarkSyncManager::Private::createFolders(GeoDataContainer *container, 
QStringList &pathList)
 
  614    GeoDataFolder *folder = 
nullptr;
 
  615    if (pathList.
count() > 0) {
 
  618        const auto otherFolders = container->folderList();
 
  619        for (GeoDataFolder *otherFolder : otherFolders) {
 
  620            if (otherFolder->name() == name) {
 
  621                folder = otherFolder;
 
  625        if (folder == 
nullptr) {
 
  626            folder = 
new GeoDataFolder();
 
  627            folder->setName(name);
 
  628            container->append(folder);
 
  636    return createFolders(folder, pathList);
 
  639GeoDataDocument *BookmarkSyncManager::Private::constructDocument(
const QList<DiffItem> &mergedList)
 
  641    auto document = 
new GeoDataDocument();
 
  642    document->setName(tr(
"Bookmarks"));
 
  644    for (
const DiffItem &item : mergedList) {
 
  645        auto placemark = 
new GeoDataPlacemark(item.m_placemarkA);
 
  647        GeoDataFolder *folder = createFolders(document, splitten);
 
  648        folder->append(placemark);
 
  654void BookmarkSyncManager::resolveConflict(MergeItem *item)
 
  658    switch (item->resolution()) {
 
  660        if (!d->m_diffA.isEmpty()) {
 
  661            diffItem = d->m_diffA.first();
 
  666        diffItem = d->m_conflictItem;
 
  672    if (diffItem.m_action != DiffItem::Deleted) {
 
  673        d->m_merged.append(diffItem);
 
  676    if (!d->m_diffA.isEmpty()) {
 
  677        d->m_diffA.removeFirst();
 
  683void BookmarkSyncManager::Private::saveDownloadedToCache(
const QByteArray &kml)
 
  685    QString localBookmarksDir = m_localBookmarksPath;
 
  686    QDir().
mkdir(localBookmarksDir.
remove(QStringLiteral(
"bookmarks.kml")));
 
  687    QFile bookmarksFile(m_localBookmarksPath);
 
  689        mDebug() << 
"Failed to open file" << bookmarksFile.fileName() << 
". It is either missing or not readable.";
 
  693    bookmarksFile.write(kml);
 
  694    bookmarksFile.close();
 
  698void BookmarkSyncManager::Private::parseTimestamp()
 
  703    m_cloudTimestamp = dataValue.
toString();
 
  704    mDebug() << 
"Remote bookmark timestamp is " << m_cloudTimestamp;
 
  705    continueSynchronization();
 
  707void BookmarkSyncManager::Private::copyLocalToCache()
 
  712    QFile bookmarksFile(m_localBookmarksPath);
 
  713    bookmarksFile.copy(QStringLiteral(
"%0/%1.kml").arg(m_cachePath, m_cloudTimestamp));
 
  717void BookmarkSyncManager::Private::continueSynchronization()
 
  719    bool cloudModified = cloudBookmarksModified(m_cloudTimestamp);
 
  723        QString lastSyncedPath = lastSyncedKmlPath();
 
  724        if (lastSyncedPath.
isEmpty()) {
 
  725            mDebug() << 
"Never synced. Uploading bookmarks.";
 
  729            bool localModified = 
false;
 
  730            for (
const DiffItem &item : std::as_const(diffList)) {
 
  731                if (item.m_action != DiffItem::NoAction) {
 
  732                    localModified = 
true;
 
  737                mDebug() << 
"Local modifications, uploading.";
 
  744void BookmarkSyncManager::Private::completeSynchronization()
 
  746    mDebug() << 
"Merging remote and local bookmark file";
 
  747    QString lastSyncedPath = lastSyncedKmlPath();
 
  748    QFile localBookmarksFile(m_localBookmarksPath);
 
  749    QByteArray result = m_downloadReply->readAll();
 
  753    if (lastSyncedPath.
isEmpty()) {
 
  754        if (localBookmarksFile.exists()) {
 
  755            mDebug() << 
"Conflict between remote bookmarks and local ones";
 
  756            m_diffA = diff(&buffer, m_localBookmarksPath);
 
  757            m_diffB = diff(m_localBookmarksPath, &buffer);
 
  759            saveDownloadedToCache(result);
 
  763        m_diffA = diff(lastSyncedPath, m_localBookmarksPath);
 
  764        m_diffB = diff(lastSyncedPath, &buffer);
 
  771void BookmarkSyncManager::Private::completeMerge()
 
  773    QFile localBookmarksFile(m_localBookmarksPath);
 
  774    GeoDataDocument *doc = constructDocument(m_merged);
 
  776    localBookmarksFile.remove();
 
  778    writer.write(&localBookmarksFile, doc);
 
  779    localBookmarksFile.close();
 
  783void BookmarkSyncManager::Private::completeUpload()
 
  788    m_cloudTimestamp = dataValue.
toString();
 
  789    mDebug() << 
"Uploaded bookmarks to remote server. Timestamp is " << m_cloudTimestamp;
 
  791    Q_EMIT m_q->syncComplete();
 
  796#include "moc_BookmarkSyncManager.cpp" 
This file contains the headers for MarbleModel.
 
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
 
QString path(const QString &relativePath)
 
QStringView merge(QStringView lhs, QStringView rhs)
 
QString name(StandardAction id)
 
Binds a QML item to a specific geodetic location in screen coordinates.
 
QByteArray & append(QByteArrayView data)
 
QStringList entryList(Filters filters, SortFlags sort) const const
 
bool mkdir(const QString &dirName) const const
 
bool mkpath(const QString &dirPath) const const
 
virtual bool open(QIODeviceBase::OpenMode mode)
 
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
 
QJsonObject object() const const
 
QJsonValue value(QLatin1StringView key) const const
 
QString toString() const const
 
void append(QList< T > &&value)
 
qsizetype count() const const
 
bool isEmpty() const const
 
QString arg(Args &&... args) const const
 
bool isEmpty() const const
 
QString & remove(QChar ch, Qt::CaseSensitivity cs)
 
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)