diff --git a/src/plugins/axivion/CMakeLists.txt b/src/plugins/axivion/CMakeLists.txt index ff089f1a98e..8e409b97119 100644 --- a/src/plugins/axivion/CMakeLists.txt +++ b/src/plugins/axivion/CMakeLists.txt @@ -13,6 +13,5 @@ add_qtc_plugin(Axivion axiviontr.h dashboard/dto.cpp dashboard/dto.h dashboard/concat.cpp dashboard/concat.h - dashboard/dashboardclient.cpp dashboard/dashboardclient.h dashboard/error.h dashboard/error.cpp ) diff --git a/src/plugins/axivion/axivion.qbs b/src/plugins/axivion/axivion.qbs index 1fb4dd7bfa8..3f82c944ce7 100644 --- a/src/plugins/axivion/axivion.qbs +++ b/src/plugins/axivion/axivion.qbs @@ -39,8 +39,6 @@ QtcPlugin { "concat.h", "dto.cpp", "dto.h", - "dashboardclient.cpp", - "dashboardclient.h", "error.cpp", "error.h", ] diff --git a/src/plugins/axivion/axivionoutputpane.cpp b/src/plugins/axivion/axivionoutputpane.cpp index 9711dadb8ff..a67c524e01c 100644 --- a/src/plugins/axivion/axivionoutputpane.cpp +++ b/src/plugins/axivion/axivionoutputpane.cpp @@ -94,10 +94,10 @@ void DashboardWidget::updateUi() delete child->widget(); delete child; } - std::shared_ptr projectInfo = Internal::projectInfo(); + std::optional projectInfo = Internal::projectInfo(); if (!projectInfo) return; - const Dto::ProjectInfoDto &info = projectInfo->data; + const Dto::ProjectInfoDto &info = *projectInfo; m_project->setText(info.name); if (info.versions.empty()) return; @@ -154,11 +154,11 @@ void DashboardWidget::updateUi() for (const Dto::Any::MapEntry &issueCount : last.issueCounts.getMap()) { if (issueCount.second.isMap()) { const Dto::Any::Map &counts = issueCount.second.getMap(); - qint64 total = extract_value(counts, QStringLiteral(u"Total")); + qint64 total = extract_value(counts, QStringLiteral("Total")); allTotal += total; - qint64 added = extract_value(counts, QStringLiteral(u"Added")); + qint64 added = extract_value(counts, QStringLiteral("Added")); allAdded += added; - qint64 removed = extract_value(counts, QStringLiteral(u"Removed")); + qint64 removed = extract_value(counts, QStringLiteral("Removed")); allRemoved += removed; addValuesWidgets(issueCount.first, total, added, removed, row); ++row; diff --git a/src/plugins/axivion/axivionplugin.cpp b/src/plugins/axivion/axivionplugin.cpp index a5a5e8979b5..e26748356af 100644 --- a/src/plugins/axivion/axivionplugin.cpp +++ b/src/plugins/axivion/axivionplugin.cpp @@ -9,8 +9,8 @@ #include "axivionresultparser.h" #include "axivionsettings.h" #include "axiviontr.h" -#include "dashboard/dashboardclient.h" #include "dashboard/dto.h" +#include "dashboard/error.h" #include #include @@ -24,11 +24,15 @@ #include #include +#include +#include + #include #include #include #include +#include #include #include #include @@ -46,6 +50,9 @@ constexpr char AxivionTextMarkId[] = "AxivionTextMark"; +using namespace Tasking; +using namespace Utils; + namespace Axivion::Internal { class AxivionPluginPrivate : public QObject @@ -55,7 +62,6 @@ public: void handleSslErrors(QNetworkReply *reply, const QList &errors); void onStartupProjectChanged(); void fetchProjectInfo(const QString &projectName); - void handleProjectInfo(DashboardClient::RawProjectInfo rawInfo); void handleOpenedDocs(ProjectExplorer::Project *project); void onDocumentOpened(Core::IDocument *doc); void onDocumentClosed(Core::IDocument * doc); @@ -63,10 +69,11 @@ public: void handleIssuesForFile(const IssuesList &issues); void fetchRuleInfo(const QString &id); - Utils::NetworkAccessManager m_networkAccessManager; + NetworkAccessManager m_networkAccessManager; AxivionOutputPane m_axivionOutputPane; - std::shared_ptr m_currentProjectInfo; + std::optional m_currentProjectInfo; bool m_runningQuery = false; + TaskTreeRunner m_taskTreeRunner; }; static AxivionPluginPrivate *dd = nullptr; @@ -74,13 +81,13 @@ static AxivionPluginPrivate *dd = nullptr; class AxivionTextMark : public TextEditor::TextMark { public: - AxivionTextMark(const Utils::FilePath &filePath, const ShortIssue &issue); + AxivionTextMark(const FilePath &filePath, const ShortIssue &issue); private: QString m_id; }; -AxivionTextMark::AxivionTextMark(const Utils::FilePath &filePath, const ShortIssue &issue) +AxivionTextMark::AxivionTextMark(const FilePath &filePath, const ShortIssue &issue) : TextEditor::TextMark(filePath, issue.lineNumber, {Tr::tr("Axivion"), AxivionTextMarkId}) , m_id(issue.id) { @@ -105,7 +112,7 @@ void fetchProjectInfo(const QString &projectName) dd->fetchProjectInfo(projectName); } -std::shared_ptr projectInfo() +std::optional projectInfo() { QTC_ASSERT(dd, return {}); return dd->m_currentProjectInfo; @@ -172,9 +179,25 @@ void AxivionPluginPrivate::onStartupProjectChanged() fetchProjectInfo(projSettings->dashboardProjectName()); } +static QUrl urlForProject(const QString &projectName) +{ + QString dashboard = settings().server.dashboard; + if (!dashboard.endsWith(QLatin1Char('/'))) + dashboard += QLatin1Char('/'); + return QUrl(dashboard).resolved(QStringLiteral("api/projects/")).resolved(projectName); +} + +static constexpr int httpStatusCodeOk = 200; +static const QLatin1String jsonContentType{ "application/json" }; + +static void deserialize(QPromise &promise, const QByteArray &input) +{ + promise.addResult(Dto::ProjectInfoDto::deserialize(input)); +} + void AxivionPluginPrivate::fetchProjectInfo(const QString &projectName) { - if (m_runningQuery) { // re-schedule + if (m_taskTreeRunner.isRunning()) { // TODO: cache in queue and run when task tree finished QTimer::singleShot(3000, this, [this, projectName] { fetchProjectInfo(projectName); }); return; } @@ -184,17 +207,94 @@ void AxivionPluginPrivate::fetchProjectInfo(const QString &projectName) m_axivionOutputPane.updateDashboard(); return; } - m_runningQuery = true; - DashboardClient client { this->m_networkAccessManager }; - QFuture response = client.fetchProjectInfo(projectName); - auto responseWatcher = std::make_shared>(); - connect(responseWatcher.get(), - &QFutureWatcher::finished, - this, - [this, responseWatcher]() { - handleProjectInfo(responseWatcher->result()); - }); - responseWatcher->setFuture(response); + + const QUrl url = urlForProject(projectName); + + const Storage storage; + + const auto onQuerySetup = [this, url](NetworkQuery &query) { + QNetworkRequest request(url); + request.setRawHeader(QByteArrayLiteral("Accept"), + QByteArray(jsonContentType.data(), jsonContentType.size())); + request.setRawHeader(QByteArrayLiteral("Authorization"), + QByteArrayLiteral("AxToken ") + settings().server.token.toUtf8()); + const QByteArray ua = QByteArrayLiteral("Axivion") + + QCoreApplication::applicationName().toUtf8() + + QByteArrayLiteral("Plugin/") + + QCoreApplication::applicationVersion().toUtf8(); + request.setRawHeader(QByteArrayLiteral("X-Axivion-User-Agent"), ua); + query.setRequest(request); + query.setNetworkAccessManager(&m_networkAccessManager); + }; + + const auto onQueryDone = [storage, url](const NetworkQuery &query, DoneWith doneWith) { + QNetworkReply *reply = query.reply(); + const QNetworkReply::NetworkError error = reply->error(); + const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + const QString contentType = reply->header(QNetworkRequest::ContentTypeHeader) + .toString() + .split(';') + .constFirst() + .trimmed() + .toLower(); + if (doneWith == DoneWith::Success && statusCode == httpStatusCodeOk + && contentType == jsonContentType) { + *storage = reply->readAll(); + return DoneResult::Success; + } + + const auto getError = [&]() -> Error { + if (contentType == jsonContentType) { + try { + return DashboardError(reply->url(), statusCode, + reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(), + Dto::ErrorDto::deserialize(reply->readAll())); + } catch (const Dto::invalid_dto_exception &) { + // ignore + } + } + if (statusCode != 0) { + return HttpError(reply->url(), statusCode, + reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(), + QString::fromUtf8(reply->readAll())); // encoding? + } + return NetworkError(reply->url(), error, reply->errorString()); + }; + + Core::MessageManager::writeFlashing( + QStringLiteral("Axivion: %1").arg(getError().message())); + return DoneResult::Error; + }; + + const auto onDeserializeSetup = [storage](Async &task) { + task.setFutureSynchronizer(ExtensionSystem::PluginManager::futureSynchronizer()); + task.setConcurrentCallData(deserialize, *storage); + }; + + const auto onDeserializeDone = [this, url](const Async &task, + DoneWith doneWith) { + if (doneWith == DoneWith::Success) { + m_currentProjectInfo = task.future().result(); + m_axivionOutputPane.updateDashboard(); + // handle already opened documents + if (auto buildSystem = ProjectExplorer::ProjectManager::startupBuildSystem(); + !buildSystem || !buildSystem->isParsing()) { + handleOpenedDocs(nullptr); + } else { + connect(ProjectExplorer::ProjectManager::instance(), + &ProjectExplorer::ProjectManager::projectFinishedParsing, + this, &AxivionPluginPrivate::handleOpenedDocs); + } + } + }; + + const Group recipe { + storage, + NetworkQueryTask(onQuerySetup, onQueryDone), + AsyncTask(onDeserializeSetup, onDeserializeDone) + }; + + m_taskTreeRunner.start(recipe); } void AxivionPluginPrivate::fetchRuleInfo(const QString &id) @@ -237,27 +337,6 @@ void AxivionPluginPrivate::clearAllMarks() onDocumentClosed(doc); } -void AxivionPluginPrivate::handleProjectInfo(DashboardClient::RawProjectInfo rawInfo) -{ - m_runningQuery = false; - if (!rawInfo) { - Core::MessageManager::writeFlashing( - QStringLiteral(u"Axivion: %1").arg(rawInfo.error().message())); - return; - } - m_currentProjectInfo = std::make_shared(std::move(rawInfo.value())); - m_axivionOutputPane.updateDashboard(); - // handle already opened documents - if (auto buildSystem = ProjectExplorer::ProjectManager::startupBuildSystem(); - !buildSystem || !buildSystem->isParsing()) { - handleOpenedDocs(nullptr); - } else { - connect(ProjectExplorer::ProjectManager::instance(), - &ProjectExplorer::ProjectManager::projectFinishedParsing, - this, &AxivionPluginPrivate::handleOpenedDocs); - } -} - void AxivionPluginPrivate::onDocumentOpened(Core::IDocument *doc) { if (!m_currentProjectInfo) // we do not have a project info (yet) @@ -267,10 +346,10 @@ void AxivionPluginPrivate::onDocumentOpened(Core::IDocument *doc) if (!doc || !project->isKnownFile(doc->filePath())) return; - Utils::FilePath relative = doc->filePath().relativeChildPath(project->projectDirectory()); + const FilePath relative = doc->filePath().relativeChildPath(project->projectDirectory()); // for now only style violations - AxivionQuery query(AxivionQuery::IssuesForFileList, {m_currentProjectInfo->data.name, "SV", - relative.path() } ); + const AxivionQuery query(AxivionQuery::IssuesForFileList, {m_currentProjectInfo->name, "SV", + relative.path()}); AxivionQueryRunner *runner = new AxivionQueryRunner(query, this); connect(runner, &AxivionQueryRunner::resultRetrieved, this, [this](const QByteArray &result){ handleIssuesForFile(ResultParser::parseIssuesList(result)); @@ -301,10 +380,10 @@ void AxivionPluginPrivate::handleIssuesForFile(const IssuesList &issues) if (!project) return; - const Utils::FilePath filePath = project->projectDirectory() + const FilePath filePath = project->projectDirectory() .pathAppended(issues.issues.first().filePath); - const Utils::Id axivionId(AxivionTextMarkId); + const Id axivionId(AxivionTextMarkId); for (const ShortIssue &issue : std::as_const(issues.issues)) { // FIXME the line location can be wrong (even the whole issue could be wrong) // depending on whether this line has been changed since the last axivion run and the diff --git a/src/plugins/axivion/axivionplugin.h b/src/plugins/axivion/axivionplugin.h index 52c2062367a..202d731b65e 100644 --- a/src/plugins/axivion/axivionplugin.h +++ b/src/plugins/axivion/axivionplugin.h @@ -3,7 +3,7 @@ #pragma once -#include "dashboard/dashboardclient.h" +#include "dashboard/dto.h" #include @@ -12,7 +12,7 @@ namespace ProjectExplorer { class Project; } namespace Axivion::Internal { void fetchProjectInfo(const QString &projectName); -std::shared_ptr projectInfo(); +std::optional projectInfo(); bool handleCertificateIssue(); } // Axivion::Internal diff --git a/src/plugins/axivion/dashboard/dashboardclient.cpp b/src/plugins/axivion/dashboard/dashboardclient.cpp deleted file mode 100644 index 34942de78d5..00000000000 --- a/src/plugins/axivion/dashboard/dashboardclient.cpp +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright (C) 2022-current by Axivion GmbH - * https://www.axivion.com/ - * - * SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - */ - -#include "dashboardclient.h" - -#include "axivionsettings.h" - -#include -#include -#include -#include -#include -#include -#include - -#include - -namespace Axivion::Internal -{ - -Credential::Credential(const QString &apiToken) - : m_authorizationValue(QByteArrayLiteral("AxToken ") + apiToken.toUtf8()) -{ -} - -const QByteArray &Credential::authorizationValue() const -{ - return m_authorizationValue; -} - -QFuture CredentialProvider::getCredential() -{ - return QtFuture::makeReadyFuture(Credential(settings().server.token)); -} - -QFuture CredentialProvider::authenticationFailure(const Credential &credential) -{ - Q_UNUSED(credential); - // ToDo: invalidate stored credential to prevent further accesses with it. - // This is to prevent account locking on password change day due to to many - // authentication failuers caused by automated requests. - return QtFuture::makeReadyFuture(); -} - -QFuture CredentialProvider::authenticationSuccess(const Credential &credential) -{ - Q_UNUSED(credential); - // ToDo: store (now verified) credential on disk if not already happened. - return QtFuture::makeReadyFuture(); -} - -bool CredentialProvider::canReRequestPasswordOnAuthenticationFailure() -{ - // ToDo: support on-demand password input dialog. - return false; -} - -ClientData::ClientData(Utils::NetworkAccessManager &networkAccessManager) - : networkAccessManager(networkAccessManager), - credentialProvider(std::make_unique()) -{ -} - -DashboardClient::DashboardClient(Utils::NetworkAccessManager &networkAccessManager) - : m_clientData(std::make_shared(networkAccessManager)) -{ -} - -using ResponseData = Utils::expected, Error>; - -static constexpr int httpStatusCodeOk = 200; -static const QLatin1String jsonContentType{ "application/json" }; - -static ResponseData readResponse(QNetworkReply &reply, QAnyStringView expectedContentType) -{ - QNetworkReply::NetworkError error = reply.error(); - int statusCode = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - QString contentType = reply.header(QNetworkRequest::ContentTypeHeader) - .toString() - .split(';') - .constFirst() - .trimmed() - .toLower(); - if (error == QNetworkReply::NetworkError::NoError - && statusCode == httpStatusCodeOk - && contentType == expectedContentType) { - return DataWithOrigin(reply.url(), reply.readAll()); - } - if (contentType == jsonContentType) { - try { - return tl::make_unexpected(DashboardError( - reply.url(), - statusCode, - reply.attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(), - Dto::ErrorDto::deserialize(reply.readAll()))); - } catch (const Dto::invalid_dto_exception &) { - // ignore - } - } - if (statusCode != 0) { - return tl::make_unexpected(HttpError( - reply.url(), - statusCode, - reply.attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(), - QString::fromUtf8(reply.readAll()))); // encoding? - } - return tl::make_unexpected( - NetworkError(reply.url(), error, reply.errorString())); -} - -template -static Utils::expected, Error> parseResponse(ResponseData rawBody) -{ - if (!rawBody) - return tl::make_unexpected(std::move(rawBody.error())); - try { - T data = T::deserialize(rawBody.value().data); - return DataWithOrigin(std::move(rawBody.value().origin), - std::move(data)); - } catch (const Dto::invalid_dto_exception &e) { - return tl::make_unexpected(GeneralError(std::move(rawBody.value().origin), - QString::fromUtf8(e.what()))); - } -} - -static void fetch(QPromise promise, std::shared_ptr clientData, QUrl url); - -static void processResponse(QPromise promise, - std::shared_ptr clientData, - QNetworkReply *reply, - Credential credential) -{ - ResponseData response = readResponse(*reply, jsonContentType); - if (!response - && response.error().isInvalidCredentialsError() - && clientData->credentialProvider->canReRequestPasswordOnAuthenticationFailure()) { - QFutureWatcher *watcher = new QFutureWatcher(&clientData->networkAccessManager); - QObject::connect(watcher, - &QFutureWatcher::finished, - &clientData->networkAccessManager, - [promise = std::move(promise), - clientData, - url = reply->url(), - watcher]() mutable { - fetch(std::move(promise), std::move(clientData), std::move(url)); - watcher->deleteLater(); - }); - watcher->setFuture(clientData->credentialProvider->authenticationFailure(credential)); - return; - } - if (response) { - clientData->credentialProvider->authenticationSuccess(credential); - } else if (response.error().isInvalidCredentialsError()) { - clientData->credentialProvider->authenticationFailure(credential); - } - promise.addResult(std::move(response)); - promise.finish(); -} - -static void fetch(QPromise promise, - std::shared_ptr clientData, - const QUrl &url, - Credential credential) -{ - QNetworkRequest request{ url }; - request.setRawHeader(QByteArrayLiteral("Accept"), - QByteArray(jsonContentType.data(), jsonContentType.size())); - request.setRawHeader(QByteArrayLiteral("Authorization"), - credential.authorizationValue()); - QByteArray ua = QByteArrayLiteral("Axivion") - + QCoreApplication::applicationName().toUtf8() - + QByteArrayLiteral("Plugin/") - + QCoreApplication::applicationVersion().toUtf8(); - request.setRawHeader(QByteArrayLiteral("X-Axivion-User-Agent"), ua); - QNetworkReply *reply = clientData->networkAccessManager.get(request); - QObject::connect(reply, - &QNetworkReply::finished, - reply, - [promise = std::move(promise), - clientData = std::move(clientData), - reply, - credential = std::move(credential)]() mutable { - processResponse(std::move(promise), - std::move(clientData), - reply, - std::move(credential)); - reply->deleteLater(); - }); -} - -static void fetch(QPromise promise, - std::shared_ptr clientData, - QUrl url) -{ - QFutureWatcher *watcher = new QFutureWatcher(&clientData->networkAccessManager); - QObject::connect(watcher, - &QFutureWatcher::finished, - &clientData->networkAccessManager, - [promise = std::move(promise), clientData, url = std::move(url), watcher]() mutable { - fetch(std::move(promise), - std::move(clientData), - url, - watcher->result()); - watcher->deleteLater();; - }); - watcher->setFuture(clientData->credentialProvider->getCredential()); -} - -static QFuture fetch(std::shared_ptr clientData, - const std::optional &base, - const QUrl &target) -{ - QPromise promise; - promise.start(); - QFuture future = promise.future(); - fetch(std::move(promise), - std::move(clientData), - base ? base->resolved(target) : target); - return future; -} - -QFuture DashboardClient::fetchProjectInfo(const QString &projectName) -{ - const AxivionServer &server = settings().server; - QString dashboard = server.dashboard; - if (!dashboard.endsWith(QLatin1Char('/'))) - dashboard += QLatin1Char('/'); - QUrl url = QUrl(dashboard) - .resolved(QUrl(QStringLiteral(u"api/projects/"))) - .resolved(QUrl(projectName)); - return fetch(this->m_clientData, std::nullopt, url) - .then(QtFuture::Launch::Async, &parseResponse); -} - -} // namespace Axivion::Internal diff --git a/src/plugins/axivion/dashboard/dashboardclient.h b/src/plugins/axivion/dashboard/dashboardclient.h deleted file mode 100644 index bdb44ecaea7..00000000000 --- a/src/plugins/axivion/dashboard/dashboardclient.h +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once - -/* - * Copyright (C) 2022-current by Axivion GmbH - * https://www.axivion.com/ - * - * SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - */ - -#include "dashboard/dto.h" -#include "dashboard/error.h" - -#include -#include - -#include -#include - -namespace Axivion::Internal -{ - -template -class DataWithOrigin -{ -public: - QUrl origin; - T data; - - DataWithOrigin(QUrl origin, T data) : origin(std::move(origin)), data(std::move(data)) { } -}; - -class Credential -{ -public: - Credential(const QString &apiToken); - - const QByteArray &authorizationValue() const; - -private: - QByteArray m_authorizationValue; -}; - -class CredentialProvider -{ -public: - QFuture getCredential(); - - QFuture authenticationFailure(const Credential &credential); - - QFuture authenticationSuccess(const Credential &credential); - - bool canReRequestPasswordOnAuthenticationFailure(); -}; - -class ClientData -{ -public: - Utils::NetworkAccessManager &networkAccessManager; - std::unique_ptr credentialProvider; - - ClientData(Utils::NetworkAccessManager &networkAccessManager); -}; - -class DashboardClient -{ -public: - using ProjectInfo = DataWithOrigin; - using RawProjectInfo = Utils::expected; - - DashboardClient(Utils::NetworkAccessManager &networkAccessManager); - - QFuture fetchProjectInfo(const QString &projectName); - -private: - std::shared_ptr m_clientData; -}; - -} // namespace Axivion::Internal diff --git a/src/plugins/axivion/dashboard/error.cpp b/src/plugins/axivion/dashboard/error.cpp index d28bf46bc3c..66cb032fd1f 100644 --- a/src/plugins/axivion/dashboard/error.cpp +++ b/src/plugins/axivion/dashboard/error.cpp @@ -76,25 +76,25 @@ QString Error::message() const return std::visit( overloaded{ [](const GeneralError &error) { - return QStringLiteral(u"GeneralError (%1) %2") + return QStringLiteral("GeneralError (%1) %2") .arg(error.replyUrl.toString(), error.message); }, [](const NetworkError &error) { - return QStringLiteral(u"NetworkError (%1) %2: %3") + return QStringLiteral("NetworkError (%1) %2: %3") .arg(error.replyUrl.toString(), QString::number(error.networkError), error.networkErrorString); }, [](const HttpError &error) { - return QStringLiteral(u"HttpError (%1) %2: %3\n%4") + return QStringLiteral("HttpError (%1) %2: %3\n%4") .arg(error.replyUrl.toString(), QString::number(error.httpStatusCode), error.httpReasonPhrase, error.body); }, [](const DashboardError &error) { - return QStringLiteral(u"DashboardError (%1) [%2 %3] %4: %5") + return QStringLiteral("DashboardError (%1) [%2 %3] %4: %5") .arg(error.replyUrl.toString(), QString::number(error.httpStatusCode), error.httpReasonPhrase,