forked from qt-creator/qt-creator
Axivion: Employ TaskTree
Change-Id: I55678f67d990c569073571055dbfe820b2fd324e Reviewed-by: <github-actions-qt-creator@cristianadam.eu> Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
@@ -13,6 +13,5 @@ add_qtc_plugin(Axivion
|
|||||||
axiviontr.h
|
axiviontr.h
|
||||||
dashboard/dto.cpp dashboard/dto.h
|
dashboard/dto.cpp dashboard/dto.h
|
||||||
dashboard/concat.cpp dashboard/concat.h
|
dashboard/concat.cpp dashboard/concat.h
|
||||||
dashboard/dashboardclient.cpp dashboard/dashboardclient.h
|
|
||||||
dashboard/error.h dashboard/error.cpp
|
dashboard/error.h dashboard/error.cpp
|
||||||
)
|
)
|
||||||
|
@@ -39,8 +39,6 @@ QtcPlugin {
|
|||||||
"concat.h",
|
"concat.h",
|
||||||
"dto.cpp",
|
"dto.cpp",
|
||||||
"dto.h",
|
"dto.h",
|
||||||
"dashboardclient.cpp",
|
|
||||||
"dashboardclient.h",
|
|
||||||
"error.cpp",
|
"error.cpp",
|
||||||
"error.h",
|
"error.h",
|
||||||
]
|
]
|
||||||
|
@@ -94,10 +94,10 @@ void DashboardWidget::updateUi()
|
|||||||
delete child->widget();
|
delete child->widget();
|
||||||
delete child;
|
delete child;
|
||||||
}
|
}
|
||||||
std::shared_ptr<const DashboardClient::ProjectInfo> projectInfo = Internal::projectInfo();
|
std::optional<Dto::ProjectInfoDto> projectInfo = Internal::projectInfo();
|
||||||
if (!projectInfo)
|
if (!projectInfo)
|
||||||
return;
|
return;
|
||||||
const Dto::ProjectInfoDto &info = projectInfo->data;
|
const Dto::ProjectInfoDto &info = *projectInfo;
|
||||||
m_project->setText(info.name);
|
m_project->setText(info.name);
|
||||||
if (info.versions.empty())
|
if (info.versions.empty())
|
||||||
return;
|
return;
|
||||||
@@ -154,11 +154,11 @@ void DashboardWidget::updateUi()
|
|||||||
for (const Dto::Any::MapEntry &issueCount : last.issueCounts.getMap()) {
|
for (const Dto::Any::MapEntry &issueCount : last.issueCounts.getMap()) {
|
||||||
if (issueCount.second.isMap()) {
|
if (issueCount.second.isMap()) {
|
||||||
const Dto::Any::Map &counts = issueCount.second.getMap();
|
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;
|
allTotal += total;
|
||||||
qint64 added = extract_value(counts, QStringLiteral(u"Added"));
|
qint64 added = extract_value(counts, QStringLiteral("Added"));
|
||||||
allAdded += added;
|
allAdded += added;
|
||||||
qint64 removed = extract_value(counts, QStringLiteral(u"Removed"));
|
qint64 removed = extract_value(counts, QStringLiteral("Removed"));
|
||||||
allRemoved += removed;
|
allRemoved += removed;
|
||||||
addValuesWidgets(issueCount.first, total, added, removed, row);
|
addValuesWidgets(issueCount.first, total, added, removed, row);
|
||||||
++row;
|
++row;
|
||||||
|
@@ -9,8 +9,8 @@
|
|||||||
#include "axivionresultparser.h"
|
#include "axivionresultparser.h"
|
||||||
#include "axivionsettings.h"
|
#include "axivionsettings.h"
|
||||||
#include "axiviontr.h"
|
#include "axiviontr.h"
|
||||||
#include "dashboard/dashboardclient.h"
|
|
||||||
#include "dashboard/dto.h"
|
#include "dashboard/dto.h"
|
||||||
|
#include "dashboard/error.h"
|
||||||
|
|
||||||
#include <coreplugin/editormanager/documentmodel.h>
|
#include <coreplugin/editormanager/documentmodel.h>
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
@@ -24,11 +24,15 @@
|
|||||||
#include <projectexplorer/project.h>
|
#include <projectexplorer/project.h>
|
||||||
#include <projectexplorer/projectmanager.h>
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
|
||||||
|
#include <solutions/tasking/networkquery.h>
|
||||||
|
#include <solutions/tasking/tasktreerunner.h>
|
||||||
|
|
||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
#include <texteditor/textmark.h>
|
#include <texteditor/textmark.h>
|
||||||
|
|
||||||
#include <utils/algorithm.h>
|
#include <utils/algorithm.h>
|
||||||
|
#include <utils/async.h>
|
||||||
#include <utils/expected.h>
|
#include <utils/expected.h>
|
||||||
#include <utils/networkaccessmanager.h>
|
#include <utils/networkaccessmanager.h>
|
||||||
#include <utils/qtcassert.h>
|
#include <utils/qtcassert.h>
|
||||||
@@ -46,6 +50,9 @@
|
|||||||
|
|
||||||
constexpr char AxivionTextMarkId[] = "AxivionTextMark";
|
constexpr char AxivionTextMarkId[] = "AxivionTextMark";
|
||||||
|
|
||||||
|
using namespace Tasking;
|
||||||
|
using namespace Utils;
|
||||||
|
|
||||||
namespace Axivion::Internal {
|
namespace Axivion::Internal {
|
||||||
|
|
||||||
class AxivionPluginPrivate : public QObject
|
class AxivionPluginPrivate : public QObject
|
||||||
@@ -55,7 +62,6 @@ public:
|
|||||||
void handleSslErrors(QNetworkReply *reply, const QList<QSslError> &errors);
|
void handleSslErrors(QNetworkReply *reply, const QList<QSslError> &errors);
|
||||||
void onStartupProjectChanged();
|
void onStartupProjectChanged();
|
||||||
void fetchProjectInfo(const QString &projectName);
|
void fetchProjectInfo(const QString &projectName);
|
||||||
void handleProjectInfo(DashboardClient::RawProjectInfo rawInfo);
|
|
||||||
void handleOpenedDocs(ProjectExplorer::Project *project);
|
void handleOpenedDocs(ProjectExplorer::Project *project);
|
||||||
void onDocumentOpened(Core::IDocument *doc);
|
void onDocumentOpened(Core::IDocument *doc);
|
||||||
void onDocumentClosed(Core::IDocument * doc);
|
void onDocumentClosed(Core::IDocument * doc);
|
||||||
@@ -63,10 +69,11 @@ public:
|
|||||||
void handleIssuesForFile(const IssuesList &issues);
|
void handleIssuesForFile(const IssuesList &issues);
|
||||||
void fetchRuleInfo(const QString &id);
|
void fetchRuleInfo(const QString &id);
|
||||||
|
|
||||||
Utils::NetworkAccessManager m_networkAccessManager;
|
NetworkAccessManager m_networkAccessManager;
|
||||||
AxivionOutputPane m_axivionOutputPane;
|
AxivionOutputPane m_axivionOutputPane;
|
||||||
std::shared_ptr<const DashboardClient::ProjectInfo> m_currentProjectInfo;
|
std::optional<Dto::ProjectInfoDto> m_currentProjectInfo;
|
||||||
bool m_runningQuery = false;
|
bool m_runningQuery = false;
|
||||||
|
TaskTreeRunner m_taskTreeRunner;
|
||||||
};
|
};
|
||||||
|
|
||||||
static AxivionPluginPrivate *dd = nullptr;
|
static AxivionPluginPrivate *dd = nullptr;
|
||||||
@@ -74,13 +81,13 @@ static AxivionPluginPrivate *dd = nullptr;
|
|||||||
class AxivionTextMark : public TextEditor::TextMark
|
class AxivionTextMark : public TextEditor::TextMark
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AxivionTextMark(const Utils::FilePath &filePath, const ShortIssue &issue);
|
AxivionTextMark(const FilePath &filePath, const ShortIssue &issue);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_id;
|
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})
|
: TextEditor::TextMark(filePath, issue.lineNumber, {Tr::tr("Axivion"), AxivionTextMarkId})
|
||||||
, m_id(issue.id)
|
, m_id(issue.id)
|
||||||
{
|
{
|
||||||
@@ -105,7 +112,7 @@ void fetchProjectInfo(const QString &projectName)
|
|||||||
dd->fetchProjectInfo(projectName);
|
dd->fetchProjectInfo(projectName);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<const DashboardClient::ProjectInfo> projectInfo()
|
std::optional<Dto::ProjectInfoDto> projectInfo()
|
||||||
{
|
{
|
||||||
QTC_ASSERT(dd, return {});
|
QTC_ASSERT(dd, return {});
|
||||||
return dd->m_currentProjectInfo;
|
return dd->m_currentProjectInfo;
|
||||||
@@ -172,9 +179,25 @@ void AxivionPluginPrivate::onStartupProjectChanged()
|
|||||||
fetchProjectInfo(projSettings->dashboardProjectName());
|
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<Dto::ProjectInfoDto> &promise, const QByteArray &input)
|
||||||
|
{
|
||||||
|
promise.addResult(Dto::ProjectInfoDto::deserialize(input));
|
||||||
|
}
|
||||||
|
|
||||||
void AxivionPluginPrivate::fetchProjectInfo(const QString &projectName)
|
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); });
|
QTimer::singleShot(3000, this, [this, projectName] { fetchProjectInfo(projectName); });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -184,17 +207,94 @@ void AxivionPluginPrivate::fetchProjectInfo(const QString &projectName)
|
|||||||
m_axivionOutputPane.updateDashboard();
|
m_axivionOutputPane.updateDashboard();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_runningQuery = true;
|
|
||||||
DashboardClient client { this->m_networkAccessManager };
|
const QUrl url = urlForProject(projectName);
|
||||||
QFuture<DashboardClient::RawProjectInfo> response = client.fetchProjectInfo(projectName);
|
|
||||||
auto responseWatcher = std::make_shared<QFutureWatcher<DashboardClient::RawProjectInfo>>();
|
const Storage<QByteArray> storage;
|
||||||
connect(responseWatcher.get(),
|
|
||||||
&QFutureWatcher<DashboardClient::RawProjectInfo>::finished,
|
const auto onQuerySetup = [this, url](NetworkQuery &query) {
|
||||||
this,
|
QNetworkRequest request(url);
|
||||||
[this, responseWatcher]() {
|
request.setRawHeader(QByteArrayLiteral("Accept"),
|
||||||
handleProjectInfo(responseWatcher->result());
|
QByteArray(jsonContentType.data(), jsonContentType.size()));
|
||||||
});
|
request.setRawHeader(QByteArrayLiteral("Authorization"),
|
||||||
responseWatcher->setFuture(response);
|
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<Dto::ProjectInfoDto> &task) {
|
||||||
|
task.setFutureSynchronizer(ExtensionSystem::PluginManager::futureSynchronizer());
|
||||||
|
task.setConcurrentCallData(deserialize, *storage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto onDeserializeDone = [this, url](const Async<Dto::ProjectInfoDto> &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<Dto::ProjectInfoDto>(onDeserializeSetup, onDeserializeDone)
|
||||||
|
};
|
||||||
|
|
||||||
|
m_taskTreeRunner.start(recipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AxivionPluginPrivate::fetchRuleInfo(const QString &id)
|
void AxivionPluginPrivate::fetchRuleInfo(const QString &id)
|
||||||
@@ -237,27 +337,6 @@ void AxivionPluginPrivate::clearAllMarks()
|
|||||||
onDocumentClosed(doc);
|
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<const DashboardClient::ProjectInfo>(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)
|
void AxivionPluginPrivate::onDocumentOpened(Core::IDocument *doc)
|
||||||
{
|
{
|
||||||
if (!m_currentProjectInfo) // we do not have a project info (yet)
|
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()))
|
if (!doc || !project->isKnownFile(doc->filePath()))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Utils::FilePath relative = doc->filePath().relativeChildPath(project->projectDirectory());
|
const FilePath relative = doc->filePath().relativeChildPath(project->projectDirectory());
|
||||||
// for now only style violations
|
// for now only style violations
|
||||||
AxivionQuery query(AxivionQuery::IssuesForFileList, {m_currentProjectInfo->data.name, "SV",
|
const AxivionQuery query(AxivionQuery::IssuesForFileList, {m_currentProjectInfo->name, "SV",
|
||||||
relative.path() } );
|
relative.path()});
|
||||||
AxivionQueryRunner *runner = new AxivionQueryRunner(query, this);
|
AxivionQueryRunner *runner = new AxivionQueryRunner(query, this);
|
||||||
connect(runner, &AxivionQueryRunner::resultRetrieved, this, [this](const QByteArray &result){
|
connect(runner, &AxivionQueryRunner::resultRetrieved, this, [this](const QByteArray &result){
|
||||||
handleIssuesForFile(ResultParser::parseIssuesList(result));
|
handleIssuesForFile(ResultParser::parseIssuesList(result));
|
||||||
@@ -301,10 +380,10 @@ void AxivionPluginPrivate::handleIssuesForFile(const IssuesList &issues)
|
|||||||
if (!project)
|
if (!project)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const Utils::FilePath filePath = project->projectDirectory()
|
const FilePath filePath = project->projectDirectory()
|
||||||
.pathAppended(issues.issues.first().filePath);
|
.pathAppended(issues.issues.first().filePath);
|
||||||
|
|
||||||
const Utils::Id axivionId(AxivionTextMarkId);
|
const Id axivionId(AxivionTextMarkId);
|
||||||
for (const ShortIssue &issue : std::as_const(issues.issues)) {
|
for (const ShortIssue &issue : std::as_const(issues.issues)) {
|
||||||
// FIXME the line location can be wrong (even the whole issue could be wrong)
|
// 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
|
// depending on whether this line has been changed since the last axivion run and the
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "dashboard/dashboardclient.h"
|
#include "dashboard/dto.h"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ namespace ProjectExplorer { class Project; }
|
|||||||
namespace Axivion::Internal {
|
namespace Axivion::Internal {
|
||||||
|
|
||||||
void fetchProjectInfo(const QString &projectName);
|
void fetchProjectInfo(const QString &projectName);
|
||||||
std::shared_ptr<const DashboardClient::ProjectInfo> projectInfo();
|
std::optional<Dto::ProjectInfoDto> projectInfo();
|
||||||
bool handleCertificateIssue();
|
bool handleCertificateIssue();
|
||||||
|
|
||||||
} // Axivion::Internal
|
} // Axivion::Internal
|
||||||
|
@@ -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 <QByteArray>
|
|
||||||
#include <QCoreApplication>
|
|
||||||
#include <QFutureWatcher>
|
|
||||||
#include <QLatin1String>
|
|
||||||
#include <QNetworkReply>
|
|
||||||
#include <QNetworkRequest>
|
|
||||||
#include <QPromise>
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
namespace Axivion::Internal
|
|
||||||
{
|
|
||||||
|
|
||||||
Credential::Credential(const QString &apiToken)
|
|
||||||
: m_authorizationValue(QByteArrayLiteral("AxToken ") + apiToken.toUtf8())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
const QByteArray &Credential::authorizationValue() const
|
|
||||||
{
|
|
||||||
return m_authorizationValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFuture<Credential> CredentialProvider::getCredential()
|
|
||||||
{
|
|
||||||
return QtFuture::makeReadyFuture(Credential(settings().server.token));
|
|
||||||
}
|
|
||||||
|
|
||||||
QFuture<void> 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<void> 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<CredentialProvider>())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
DashboardClient::DashboardClient(Utils::NetworkAccessManager &networkAccessManager)
|
|
||||||
: m_clientData(std::make_shared<ClientData>(networkAccessManager))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
using ResponseData = Utils::expected<DataWithOrigin<QByteArray>, 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<typename T>
|
|
||||||
static Utils::expected<DataWithOrigin<T>, 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<ResponseData> promise, std::shared_ptr<ClientData> clientData, QUrl url);
|
|
||||||
|
|
||||||
static void processResponse(QPromise<ResponseData> promise,
|
|
||||||
std::shared_ptr<ClientData> clientData,
|
|
||||||
QNetworkReply *reply,
|
|
||||||
Credential credential)
|
|
||||||
{
|
|
||||||
ResponseData response = readResponse(*reply, jsonContentType);
|
|
||||||
if (!response
|
|
||||||
&& response.error().isInvalidCredentialsError()
|
|
||||||
&& clientData->credentialProvider->canReRequestPasswordOnAuthenticationFailure()) {
|
|
||||||
QFutureWatcher<void> *watcher = new QFutureWatcher<void>(&clientData->networkAccessManager);
|
|
||||||
QObject::connect(watcher,
|
|
||||||
&QFutureWatcher<void>::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<ResponseData> promise,
|
|
||||||
std::shared_ptr<ClientData> 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<ResponseData> promise,
|
|
||||||
std::shared_ptr<ClientData> clientData,
|
|
||||||
QUrl url)
|
|
||||||
{
|
|
||||||
QFutureWatcher<Credential> *watcher = new QFutureWatcher<Credential>(&clientData->networkAccessManager);
|
|
||||||
QObject::connect(watcher,
|
|
||||||
&QFutureWatcher<Credential>::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<ResponseData> fetch(std::shared_ptr<ClientData> clientData,
|
|
||||||
const std::optional<QUrl> &base,
|
|
||||||
const QUrl &target)
|
|
||||||
{
|
|
||||||
QPromise<ResponseData> promise;
|
|
||||||
promise.start();
|
|
||||||
QFuture<ResponseData> future = promise.future();
|
|
||||||
fetch(std::move(promise),
|
|
||||||
std::move(clientData),
|
|
||||||
base ? base->resolved(target) : target);
|
|
||||||
return future;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFuture<DashboardClient::RawProjectInfo> 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<Dto::ProjectInfoDto>);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Axivion::Internal
|
|
@@ -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 <utils/expected.h>
|
|
||||||
#include <utils/networkaccessmanager.h>
|
|
||||||
|
|
||||||
#include <QFuture>
|
|
||||||
#include <QNetworkReply>
|
|
||||||
|
|
||||||
namespace Axivion::Internal
|
|
||||||
{
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
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<Credential> getCredential();
|
|
||||||
|
|
||||||
QFuture<void> authenticationFailure(const Credential &credential);
|
|
||||||
|
|
||||||
QFuture<void> authenticationSuccess(const Credential &credential);
|
|
||||||
|
|
||||||
bool canReRequestPasswordOnAuthenticationFailure();
|
|
||||||
};
|
|
||||||
|
|
||||||
class ClientData
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
Utils::NetworkAccessManager &networkAccessManager;
|
|
||||||
std::unique_ptr<CredentialProvider> credentialProvider;
|
|
||||||
|
|
||||||
ClientData(Utils::NetworkAccessManager &networkAccessManager);
|
|
||||||
};
|
|
||||||
|
|
||||||
class DashboardClient
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using ProjectInfo = DataWithOrigin<Dto::ProjectInfoDto>;
|
|
||||||
using RawProjectInfo = Utils::expected<ProjectInfo, Error>;
|
|
||||||
|
|
||||||
DashboardClient(Utils::NetworkAccessManager &networkAccessManager);
|
|
||||||
|
|
||||||
QFuture<RawProjectInfo> fetchProjectInfo(const QString &projectName);
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::shared_ptr<ClientData> m_clientData;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Axivion::Internal
|
|
@@ -76,25 +76,25 @@ QString Error::message() const
|
|||||||
return std::visit(
|
return std::visit(
|
||||||
overloaded{
|
overloaded{
|
||||||
[](const GeneralError &error) {
|
[](const GeneralError &error) {
|
||||||
return QStringLiteral(u"GeneralError (%1) %2")
|
return QStringLiteral("GeneralError (%1) %2")
|
||||||
.arg(error.replyUrl.toString(),
|
.arg(error.replyUrl.toString(),
|
||||||
error.message);
|
error.message);
|
||||||
},
|
},
|
||||||
[](const NetworkError &error) {
|
[](const NetworkError &error) {
|
||||||
return QStringLiteral(u"NetworkError (%1) %2: %3")
|
return QStringLiteral("NetworkError (%1) %2: %3")
|
||||||
.arg(error.replyUrl.toString(),
|
.arg(error.replyUrl.toString(),
|
||||||
QString::number(error.networkError),
|
QString::number(error.networkError),
|
||||||
error.networkErrorString);
|
error.networkErrorString);
|
||||||
},
|
},
|
||||||
[](const HttpError &error) {
|
[](const HttpError &error) {
|
||||||
return QStringLiteral(u"HttpError (%1) %2: %3\n%4")
|
return QStringLiteral("HttpError (%1) %2: %3\n%4")
|
||||||
.arg(error.replyUrl.toString(),
|
.arg(error.replyUrl.toString(),
|
||||||
QString::number(error.httpStatusCode),
|
QString::number(error.httpStatusCode),
|
||||||
error.httpReasonPhrase,
|
error.httpReasonPhrase,
|
||||||
error.body);
|
error.body);
|
||||||
},
|
},
|
||||||
[](const DashboardError &error) {
|
[](const DashboardError &error) {
|
||||||
return QStringLiteral(u"DashboardError (%1) [%2 %3] %4: %5")
|
return QStringLiteral("DashboardError (%1) [%2 %3] %4: %5")
|
||||||
.arg(error.replyUrl.toString(),
|
.arg(error.replyUrl.toString(),
|
||||||
QString::number(error.httpStatusCode),
|
QString::number(error.httpStatusCode),
|
||||||
error.httpReasonPhrase,
|
error.httpReasonPhrase,
|
||||||
|
Reference in New Issue
Block a user