diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/ServerApiProvider.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/ServerApiProvider.java index 13ca25f112..7d6a4c0921 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/ServerApiProvider.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/ServerApiProvider.java @@ -21,17 +21,22 @@ import java.net.URI; import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; import javax.annotation.Nullable; import javax.inject.Named; import javax.inject.Singleton; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; +import org.sonarsource.sonarlint.core.connection.ConnectionManager; +import org.sonarsource.sonarlint.core.connection.ServerConnection; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.http.ConnectionAwareHttpClientProvider; import org.sonarsource.sonarlint.core.http.HttpClient; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; +import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.common.TransientSonarCloudConnectionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.common.TransientSonarQubeConnectionDto; @@ -42,24 +47,30 @@ import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverconnection.ServerVersionAndStatusChecker; +import org.sonarsource.sonarlint.core.storage.StorageService; +import org.sonarsource.sonarlint.core.sync.LastWebApiErrorNotificationService; import static org.apache.commons.lang.StringUtils.removeEnd; @Named @Singleton -public class ServerApiProvider { +public class ServerApiProvider implements ConnectionManager { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ConnectionConfigurationRepository connectionRepository; private final ConnectionAwareHttpClientProvider awareHttpClientProvider; private final HttpClientProvider httpClientProvider; + private final StorageService storageService; + private final SonarLintRpcClient client; private final URI sonarCloudUri; public ServerApiProvider(ConnectionConfigurationRepository connectionRepository, ConnectionAwareHttpClientProvider awareHttpClientProvider, HttpClientProvider httpClientProvider, - SonarCloudActiveEnvironment sonarCloudActiveEnvironment) { + SonarCloudActiveEnvironment sonarCloudActiveEnvironment, StorageService storageService, SonarLintRpcClient client) { this.connectionRepository = connectionRepository; this.awareHttpClientProvider = awareHttpClientProvider; this.httpClientProvider = httpClientProvider; + this.storageService = storageService; + this.client = client; this.sonarCloudUri = sonarCloudActiveEnvironment.getUri(); } @@ -100,7 +111,7 @@ public ServerApi getServerApi(String baseUrl, @Nullable String organization, Str return new ServerApi(params, httpClientProvider.getHttpClientWithPreemptiveAuth(token, isBearerSupported)); } - public ServerApi getServerApiOrThrow(String connectionId) { + private ServerApi getServerApiOrThrow(String connectionId) { var params = connectionRepository.getEndpointParams(connectionId); if (params.isEmpty()) { var error = new ResponseError(SonarLintRpcErrorCode.CONNECTION_NOT_FOUND, "Connection '" + connectionId + "' is gone", connectionId); @@ -137,4 +148,41 @@ private HttpClient getClientFor(EndpointParams params, Either httpClientProvider.getHttpClientWithPreemptiveAuth(userPass.getUsername(), userPass.getPassword())); } + @Override + public ServerConnection getConnectionOrThrow(String connectionId) { + var serverApi = getServerApiOrThrow(connectionId); + return new ServerConnection(connectionId, serverApi, new LastWebApiErrorNotificationService(storageService), client); + } + + @Override + public Optional tryGetConnection(String connectionId) { + return getServerApi(connectionId) + .map(serverApi -> new ServerConnection(connectionId, serverApi, new LastWebApiErrorNotificationService(storageService), client)); + } + + public Optional tryGetConnectionWithoutCredentials(String connectionId) { + return getServerApiWithoutCredentials(connectionId) + .map(serverApi -> new ServerConnection(connectionId, serverApi, new LastWebApiErrorNotificationService(storageService), client)); + } + + @Override + public ServerApi getTransientConnection(String token,@Nullable String organization, String baseUrl) { + return getServerApi(baseUrl, organization, token); + } + + @Override + public Optional getValidConnection(String connectionId) { + return tryGetConnection(connectionId).filter(ServerConnection::isValid); + } + + @Override + public void withValidConnection(String connectionId, Consumer serverConnectionCall) { + // wrap the consumer call which is web API call and handle the connection state to avoid spamming the server with notifications + getValidConnection(connectionId).ifPresent(serverConnectionCall); + } + + @Override + public Optional withValidConnectionAndReturn(String connectionId, Function> serverConnectionCall) { + return getValidConnection(connectionId).flatMap(serverConnectionCall); + } } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarProjectsCache.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarProjectsCache.java index 19c1b545bf..5b51c981c9 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarProjectsCache.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarProjectsCache.java @@ -120,7 +120,8 @@ public Optional getSonarProject(String connectionId, String sonar return singleProjectsCache.get(new SonarProjectKey(connectionId, sonarProjectKey), () -> { LOG.debug("Query project '{}' on connection '{}'...", sonarProjectKey, connectionId); try { - return serverApiProvider.getServerApi(connectionId).flatMap(s -> s.component().getProject(sonarProjectKey, cancelMonitor)); + return serverApiProvider.tryGetConnection(connectionId) + .flatMap(connection -> connection.withClientApiAndReturn(s -> s.component().getProject(sonarProjectKey, cancelMonitor))); } catch (Exception e) { LOG.error("Error while querying project '{}' from connection '{}'", sonarProjectKey, connectionId, e); return Optional.empty(); @@ -137,7 +138,9 @@ public TextSearchIndex getTextSearchIndex(String connectionId, So LOG.debug("Load projects from connection '{}'...", connectionId); List projects; try { - projects = serverApiProvider.getServerApi(connectionId).map(s -> s.component().getAllProjects(cancelMonitor)).orElse(List.of()); + projects = serverApiProvider.tryGetConnection(connectionId) + .map(connection -> connection.withClientApiAndReturn(s -> s.component().getAllProjects(cancelMonitor))) + .orElse(List.of()); } catch (Exception e) { LOG.error("Error while querying projects from connection '{}'", connectionId, e); return new TextSearchIndex<>(); diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelper.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelper.java index 322ee1e22c..9851c01bad 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelper.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelper.java @@ -107,23 +107,23 @@ private void queueCheckIfSoonUnsupported(String connectionId, String configScope try { var connection = connectionRepository.getConnectionById(connectionId); if (connection != null && connection.getKind() == ConnectionKind.SONARQUBE) { - var serverApi = serverApiProvider.getServerApiWithoutCredentials(connectionId); - if (serverApi.isPresent()) { - var version = synchronizationService.getServerConnection(connectionId, serverApi.get()).readOrSynchronizeServerVersion(serverApi.get(), cancelMonitor); - var isCached = cacheConnectionIdPerVersion.containsKey(connectionId) && cacheConnectionIdPerVersion.get(connectionId).compareTo(version) == 0; - if (!isCached && VersionUtils.isVersionSupportedDuringGracePeriod(version)) { - client.showSoonUnsupportedMessage( - new ShowSoonUnsupportedMessageParams( - String.format(UNSUPPORTED_NOTIFICATION_ID, connectionId, version.getName()), - configScopeId, - String.format(NOTIFICATION_MESSAGE, version.getName(), connectionId, VersionUtils.getCurrentLts()) - ) - ); - LOG.debug(String.format("Connection '%s' with version '%s' is detected to be soon unsupported", - connection.getConnectionId(), version.getName())); - } - cacheConnectionIdPerVersion.put(connectionId, version); - } + serverApiProvider.tryGetConnectionWithoutCredentials(connectionId) + .ifPresent(serverConnection -> serverConnection.withClientApi(serverApi -> { + var version = synchronizationService.readOrSynchronizeServerVersion(connectionId, serverApi, cancelMonitor); + var isCached = cacheConnectionIdPerVersion.containsKey(connectionId) && cacheConnectionIdPerVersion.get(connectionId).compareTo(version) == 0; + if (!isCached && VersionUtils.isVersionSupportedDuringGracePeriod(version)) { + client.showSoonUnsupportedMessage( + new ShowSoonUnsupportedMessageParams( + String.format(UNSUPPORTED_NOTIFICATION_ID, connectionId, version.getName()), + configScopeId, + String.format(NOTIFICATION_MESSAGE, version.getName(), connectionId, VersionUtils.getCurrentLts()) + ) + ); + LOG.debug(String.format("Connection '%s' with version '%s' is detected to be soon unsupported", + connection.getConnectionId(), version.getName())); + } + cacheConnectionIdPerVersion.put(connectionId, version); + })); } } catch (Exception e) { LOG.error("Error while checking if soon unsupported", e); diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/connection/ConnectionManager.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/connection/ConnectionManager.java new file mode 100644 index 0000000000..02974b7d6b --- /dev/null +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/connection/ConnectionManager.java @@ -0,0 +1,44 @@ +/* + * SonarLint Core - Implementation + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarsource.sonarlint.core.connection; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import javax.annotation.Nullable; +import org.sonarsource.sonarlint.core.serverapi.ServerApi; + +public interface ConnectionManager { + ServerConnection getConnectionOrThrow(String connectionId); + Optional tryGetConnection(String connectionId); + + /** + * Having dedicated TransientConnection class makes sense only if we handle the connection errors from there. + * Which brings up the problem of managing global state for notifications because we don't know the connection ID.

+ * On other hand providing ServerApis directly, all Web API calls from transient ServerApi are not protected by checks for connection state. + * So we still can spam server with unprotected requests. + * It's not a big problem because we don't use such requests during scheduled sync. + * They are mostly related to setting up the connection or other user-triggered actions. + */ + ServerApi getTransientConnection(String token, @Nullable String organization, String baseUrl); + Optional getValidConnection(String connectionId); + void withValidConnection(String connectionId, Consumer serverConnectionCall); + Optional withValidConnectionAndReturn(String connectionId, Function> serverConnectionCall); +} diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/connection/ConnectionState.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/connection/ConnectionState.java new file mode 100644 index 0000000000..b65a517bb6 --- /dev/null +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/connection/ConnectionState.java @@ -0,0 +1,24 @@ +/* + * SonarLint Core - Implementation + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarsource.sonarlint.core.connection; + +public enum ConnectionState { + NEVER_USED, ACTIVE, INVALID_CREDENTIALS, MISSING_PERMISSION, NETWORK_ERROR +} diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/connection/ServerConnection.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/connection/ServerConnection.java new file mode 100644 index 0000000000..669c137e64 --- /dev/null +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/connection/ServerConnection.java @@ -0,0 +1,106 @@ +/* + * SonarLint Core - Implementation + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarsource.sonarlint.core.connection; + +import java.time.Period; +import java.time.ZonedDateTime; +import java.util.function.Consumer; +import java.util.function.Function; +import javax.annotation.Nullable; +import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; +import org.sonarsource.sonarlint.core.rpc.protocol.client.sync.InvalidTokenParams; +import org.sonarsource.sonarlint.core.serverapi.ServerApi; +import org.sonarsource.sonarlint.core.serverapi.exception.ForbiddenException; +import org.sonarsource.sonarlint.core.serverapi.exception.UnauthorizedException; +import org.sonarsource.sonarlint.core.sync.LastWebApiErrorNotificationService; + +public class ServerConnection { + + private static final Period WRONG_TOKEN_NOTIFICATION_INTERVAL = Period.ofDays(1); + private final String connectionId; + private final ServerApi serverApi; + private final SonarLintRpcClient client; + private ConnectionState state = ConnectionState.NEVER_USED; + @Nullable + private ZonedDateTime lastNotificationTime; + private final LastWebApiErrorNotificationService lastWebApiErrorNotificationService; + + public ServerConnection(String connectionId, ServerApi serverApi, LastWebApiErrorNotificationService lastWebApiErrorNotificationService, SonarLintRpcClient client) { + this.connectionId = connectionId; + this.serverApi = serverApi; + this.lastWebApiErrorNotificationService = lastWebApiErrorNotificationService; + this.lastNotificationTime = lastWebApiErrorNotificationService.getLastWebApiErrorNotification(connectionId); + this.client = client; + } + + public boolean isSonarCloud() { + return serverApi.isSonarCloud(); + } + + public boolean isValid() { + return state == ConnectionState.NEVER_USED || state == ConnectionState.ACTIVE; + } + + public T withClientApiAndReturn(Function serverApiConsumer) { + try { + var result = serverApiConsumer.apply(serverApi); + state = ConnectionState.ACTIVE; + return result; + } catch (ForbiddenException e) { + state = ConnectionState.INVALID_CREDENTIALS; + notifyClientAboutWrongTokenIfNeeded(); + } catch (UnauthorizedException e) { + state = ConnectionState.MISSING_PERMISSION; + notifyClientAboutWrongTokenIfNeeded(); + } + return null; + } + + public void withClientApi(Consumer serverApiConsumer) { + try { + serverApiConsumer.accept(serverApi); + state = ConnectionState.ACTIVE; + } catch (ForbiddenException e) { + state = ConnectionState.INVALID_CREDENTIALS; + notifyClientAboutWrongTokenIfNeeded(); + } catch (UnauthorizedException e) { + state = ConnectionState.MISSING_PERMISSION; + notifyClientAboutWrongTokenIfNeeded(); + } + } + + private boolean shouldNotifyAboutWrongToken() { + if (state != ConnectionState.INVALID_CREDENTIALS && state != ConnectionState.MISSING_PERMISSION) { + return false; + } + if (lastNotificationTime == null) { + return true; + } + return lastNotificationTime.plus(WRONG_TOKEN_NOTIFICATION_INTERVAL).isBefore(ZonedDateTime.now()); + } + + private void notifyClientAboutWrongTokenIfNeeded() { + if (shouldNotifyAboutWrongToken()) { + client.invalidToken(new InvalidTokenParams(connectionId)); + lastNotificationTime = ZonedDateTime.now(); + lastWebApiErrorNotificationService.setLastWebApiErrorNotification(connectionId, lastNotificationTime); + } + } +} diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowHotspotRequestHandler.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowHotspotRequestHandler.java index 1b4fbab191..5dc2d0034e 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowHotspotRequestHandler.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowHotspotRequestHandler.java @@ -105,12 +105,8 @@ private void showHotspotForScope(String connectionId, String configurationScopeI } private Optional tryFetchHotspot(String connectionId, String hotspotKey, SonarLintCancelMonitor cancelMonitor) { - var serverApi = serverApiProvider.getServerApi(connectionId); - if (serverApi.isEmpty()) { - // should not happen since we found the connection just before, improve the design ? - return Optional.empty(); - } - return serverApi.get().hotspot().fetch(hotspotKey, cancelMonitor); + return serverApiProvider.tryGetConnection(connectionId) + .flatMap(connection -> connection.withClientApiAndReturn(api -> api.hotspot().fetch(hotspotKey, cancelMonitor))); } private static HotspotDetailsDto adapt(String hotspotKey, ServerHotspotDetails hotspot, FilePathTranslation translation) { diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandler.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandler.java index ec0c83a8e3..bcb4c5a1e1 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandler.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandler.java @@ -188,18 +188,14 @@ static boolean isIssueTaint(String ruleKey) { private Optional tryFetchIssue(String connectionId, String issueKey, String projectKey, String branch, @Nullable String pullRequest, SonarLintCancelMonitor cancelMonitor) { - var serverApi = serverApiProvider.getServerApiOrThrow(connectionId); - return serverApi.issue().fetchServerIssue(issueKey, projectKey, branch, pullRequest, cancelMonitor); + return serverApiProvider.getConnectionOrThrow(connectionId) + .withClientApiAndReturn(serverApi -> serverApi.issue().fetchServerIssue(issueKey, projectKey, branch, pullRequest, cancelMonitor)); } private Optional tryFetchCodeSnippet(String connectionId, String fileKey, Common.TextRange textRange, String branch, @Nullable String pullRequest, SonarLintCancelMonitor cancelMonitor) { - var serverApi = serverApiProvider.getServerApi(connectionId); - if (serverApi.isEmpty() || fileKey.isEmpty()) { - // should not happen since we found the connection just before, improve the design ? - return Optional.empty(); - } - return serverApi.get().issue().getCodeSnippet(fileKey, textRange, branch, pullRequest, cancelMonitor); + return serverApiProvider.tryGetConnection(connectionId).flatMap(connection -> + connection.withClientApiAndReturn(api -> api.issue().getCodeSnippet(fileKey, textRange, branch, pullRequest, cancelMonitor))); } @VisibleForTesting diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/file/ServerFilePathsProvider.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/file/ServerFilePathsProvider.java index 42839ee7b3..3f39c1581b 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/file/ServerFilePathsProvider.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/file/ServerFilePathsProvider.java @@ -104,15 +104,17 @@ private Optional> getPathsFromFileCache(Binding binding) { } private Optional> fetchPathsFromServer(Binding binding, SonarLintCancelMonitor cancelMonitor) { - var serverApiOpt = serverApiProvider.getServerApi(binding.getConnectionId()); - if (serverApiOpt.isEmpty()) { + var connectionOpt = serverApiProvider.tryGetConnection(binding.getConnectionId()); + if (connectionOpt.isEmpty()) { LOG.debug("Connection '{}' does not exist", binding.getConnectionId()); return Optional.empty(); } try { - List paths = fetchPathsFromServer(serverApiOpt.get(), binding.getSonarProjectKey(), cancelMonitor); - cacheServerPaths(binding, paths); - return Optional.of(paths); + return connectionOpt.get().withClientApiAndReturn(serverApi -> { + List paths = fetchPathsFromServer(serverApi, binding.getSonarProjectKey(), cancelMonitor); + cacheServerPaths(binding, paths); + return Optional.of(paths); + }); } catch (CancellationException e) { throw e; } catch (Exception e) { diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/hotspot/HotspotService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/hotspot/HotspotService.java index d4dccbe3f0..ae3ac4e839 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/hotspot/HotspotService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/hotspot/HotspotService.java @@ -117,8 +117,8 @@ public CheckLocalDetectionSupportedResponse checkLocalDetectionSupported(String public CheckStatusChangePermittedResponse checkStatusChangePermitted(String connectionId, String hotspotKey, SonarLintCancelMonitor cancelMonitor) { // fixme add getConnectionByIdOrThrow var connection = connectionRepository.getConnectionById(connectionId); - var serverApi = serverApiProvider.getServerApiOrThrow(connectionId); - var r = serverApi.hotspot().show(hotspotKey, cancelMonitor); + var r = serverApiProvider.getConnectionOrThrow(connectionId) + .withClientApiAndReturn(serverApi -> serverApi.hotspot().show(hotspotKey, cancelMonitor)); var allowedStatuses = HotspotReviewStatus.allowedStatusesOn(connection.getKind()); // canChangeStatus is false when the 'Administer Hotspots' permission is missing // normally the 'Browse' permission is also required, but we assume it's present as the client knows the hotspot key @@ -140,12 +140,12 @@ public void changeStatus(String configurationScopeId, String hotspotKey, Hotspot LOG.debug("No binding for config scope {}", configurationScopeId); return; } - var connectionOpt = serverApiProvider.getServerApi(effectiveBindingOpt.get().getConnectionId()); + var connectionOpt = serverApiProvider.tryGetConnection(effectiveBindingOpt.get().getConnectionId()); if (connectionOpt.isEmpty()) { LOG.debug("Connection {} is gone", effectiveBindingOpt.get().getConnectionId()); return; } - connectionOpt.get().hotspot().changeStatus(hotspotKey, newStatus, cancelMonitor); + connectionOpt.get().withClientApi(serverApi -> serverApi.hotspot().changeStatus(hotspotKey, newStatus, cancelMonitor)); saveStatusInStorage(effectiveBindingOpt.get(), hotspotKey, newStatus); telemetryService.hotspotStatusChanged(); } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/issue/IssueService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/issue/IssueService.java index 811eca0b35..7cdca9b7e9 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/issue/IssueService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/issue/IssueService.java @@ -130,12 +130,12 @@ public IssueService(ConfigurationRepository configurationRepository, ServerApiPr public void changeStatus(String configurationScopeId, String issueKey, ResolutionStatus newStatus, boolean isTaintIssue, SonarLintCancelMonitor cancelMonitor) { var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); - var serverApi = serverApiProvider.getServerApiOrThrow(binding.getConnectionId()); + var serverConnection = serverApiProvider.getConnectionOrThrow(binding.getConnectionId()); var reviewStatus = transitionByResolutionStatus.get(newStatus); var projectServerIssueStore = storageService.binding(binding).findings(); boolean isServerIssue = projectServerIssueStore.containsIssue(issueKey); if (isServerIssue) { - serverApi.issue().changeStatus(issueKey, reviewStatus, cancelMonitor); + serverConnection.withClientApi(serverApi -> serverApi.issue().changeStatus(issueKey, reviewStatus, cancelMonitor)); projectServerIssueStore.updateIssueResolutionStatus(issueKey, isTaintIssue, true) .ifPresent(issue -> eventPublisher.publishEvent(new ServerIssueStatusChangedEvent(binding.getConnectionId(), binding.getSonarProjectKey(), issue))); } else { @@ -148,7 +148,8 @@ public void changeStatus(String configurationScopeId, String issueKey, Resolutio var issue = localIssueOpt.get(); issue.resolve(coreStatus); var localOnlyIssueStore = localOnlyIssueStorageService.get(); - serverApi.issue().anticipatedTransitions(binding.getSonarProjectKey(), concat(localOnlyIssueStore.loadAll(configurationScopeId), issue), cancelMonitor); + serverConnection.withClientApi(serverApi -> serverApi.issue() + .anticipatedTransitions(binding.getSonarProjectKey(), concat(localOnlyIssueStore.loadAll(configurationScopeId), issue), cancelMonitor)); localOnlyIssueStore.storeLocalOnlyIssue(configurationScopeId, issue); eventPublisher.publishEvent(new LocalOnlyIssueStatusChangedEvent(issue)); } @@ -167,8 +168,8 @@ private static List subtract(List allIssues, Lis public boolean checkAnticipatedStatusChangeSupported(String configScopeId) { var binding = configurationRepository.getEffectiveBindingOrThrow(configScopeId); var connectionId = binding.getConnectionId(); - var serverApi = serverApiProvider.getServerApiOrThrow(binding.getConnectionId()); - return checkAnticipatedStatusChangeSupported(serverApi, connectionId); + return serverApiProvider.getConnectionOrThrow(binding.getConnectionId()) + .withClientApiAndReturn(serverApi -> checkAnticipatedStatusChangeSupported(serverApi, connectionId)); } /** @@ -185,8 +186,7 @@ private boolean checkAnticipatedStatusChangeSupported(ServerApi api, String conn } public CheckStatusChangePermittedResponse checkStatusChangePermitted(String connectionId, String issueKey, SonarLintCancelMonitor cancelMonitor) { - var serverApi = serverApiProvider.getServerApiOrThrow(connectionId); - return asUUID(issueKey) + return serverApiProvider.getConnectionOrThrow(connectionId).withClientApiAndReturn(serverApi -> asUUID(issueKey) .flatMap(localOnlyIssueRepository::findByKey) .map(r -> { // For anticipated issues we currently don't get the information from SonarQube (as there is no web API @@ -203,7 +203,7 @@ public CheckStatusChangePermittedResponse checkStatusChangePermitted(String conn .orElseGet(() -> { var issue = serverApi.issue().searchByKey(issueKey, cancelMonitor); return toResponse(getAdministerIssueTransitions(issue), STATUS_CHANGE_PERMISSION_MISSING_REASON); - }); + })); } /** @@ -261,11 +261,11 @@ public void addComment(String configurationScopeId, String issueKey, String text public boolean reopenIssue(String configurationScopeId, String issueId, boolean isTaintIssue, SonarLintCancelMonitor cancelMonitor) { var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); - var serverApiConnection = serverApiProvider.getServerApiOrThrow(binding.getConnectionId()); var projectServerIssueStore = storageService.binding(binding).findings(); boolean isServerIssue = projectServerIssueStore.containsIssue(issueId); if (isServerIssue) { - return reopenServerIssue(serverApiConnection, binding, issueId, projectServerIssueStore, isTaintIssue, cancelMonitor); + return serverApiProvider.getConnectionOrThrow(binding.getConnectionId()) + .withClientApiAndReturn(serverApi -> reopenServerIssue(serverApi, binding, issueId, projectServerIssueStore, isTaintIssue, cancelMonitor)); } else { return reopenLocalIssue(issueId, configurationScopeId, cancelMonitor); } @@ -285,8 +285,8 @@ private void removeAllIssuesForFile(XodusLocalOnlyIssueStore localOnlyIssueStore var issuesForFile = localOnlyIssueStore.loadForFile(configurationScopeId, filePath); var issuesToSync = subtract(allIssues, issuesForFile); var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); - var serverConnection = serverApiProvider.getServerApiOrThrow(binding.getConnectionId()); - serverConnection.issue().anticipatedTransitions(binding.getSonarProjectKey(), issuesToSync, cancelMonitor); + serverApiProvider.getConnectionOrThrow(binding.getConnectionId()) + .withClientApi(serverApi -> serverApi.issue().anticipatedTransitions(binding.getSonarProjectKey(), issuesToSync, cancelMonitor)); } private void removeIssueOnServer(XodusLocalOnlyIssueStore localOnlyIssueStore, @@ -294,8 +294,8 @@ private void removeIssueOnServer(XodusLocalOnlyIssueStore localOnlyIssueStore, var allIssues = localOnlyIssueStore.loadAll(configurationScopeId); var issuesToSync = allIssues.stream().filter(it -> !it.getId().equals(issueId)).collect(Collectors.toList()); var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); - var serverConnection = serverApiProvider.getServerApiOrThrow(binding.getConnectionId()); - serverConnection.issue().anticipatedTransitions(binding.getSonarProjectKey(), issuesToSync, cancelMonitor); + serverApiProvider.getConnectionOrThrow(binding.getConnectionId()) + .withClientApi(serverApi -> serverApi.issue().anticipatedTransitions(binding.getSonarProjectKey(), issuesToSync, cancelMonitor)); } private void setCommentOnLocalOnlyIssue(String configurationScopeId, UUID issueId, String comment, SonarLintCancelMonitor cancelMonitor) { @@ -309,8 +309,8 @@ private void setCommentOnLocalOnlyIssue(String configurationScopeId, UUID issueI var issuesToSync = localOnlyIssueStore.loadAll(configurationScopeId); issuesToSync.replaceAll(issue -> issue.getId().equals(issueId) ? commentedIssue : issue); var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); - var serverApi = serverApiProvider.getServerApiOrThrow(binding.getConnectionId()); - serverApi.issue().anticipatedTransitions(binding.getSonarProjectKey(), issuesToSync, cancelMonitor); + serverApiProvider.getConnectionOrThrow(binding.getConnectionId()) + .withClientApi(serverApi -> serverApi.issue().anticipatedTransitions(binding.getSonarProjectKey(), issuesToSync, cancelMonitor)); localOnlyIssueStore.storeLocalOnlyIssue(configurationScopeId, commentedIssue); } } else { @@ -325,8 +325,8 @@ private static ResponseErrorException issueNotFoundException(String issueId) { private void addCommentOnServerIssue(String configurationScopeId, String issueKey, String comment, SonarLintCancelMonitor cancelMonitor) { var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); - var serverApi = serverApiProvider.getServerApiOrThrow(binding.getConnectionId()); - serverApi.issue().addComment(issueKey, comment, cancelMonitor); + serverApiProvider.getConnectionOrThrow(binding.getConnectionId()) + .withClientApi(serverApi -> serverApi.issue().addComment(issueKey, comment, cancelMonitor)); } private boolean reopenServerIssue(ServerApi connection, Binding binding, String issueId, ProjectServerIssueStore projectServerIssueStore, boolean isTaintIssue, diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/RulesService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/RulesService.java index 2a7ee35ddc..a4b3a6d99a 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/RulesService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/RulesService.java @@ -48,7 +48,6 @@ import org.sonarsource.sonarlint.core.mode.SeverityModeService; import org.sonarsource.sonarlint.core.reporting.FindingReportingService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; -import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.rules.RulesRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; @@ -94,20 +93,18 @@ public class RulesService { private final Map standaloneRuleConfig = new ConcurrentHashMap<>(); private FindingReportingService findingReportingService; private final SeverityModeService severityModeService; - private final ConnectionConfigurationRepository connectionConfigurationRepository; @Inject public RulesService(ServerApiProvider serverApiProvider, ConfigurationRepository configurationRepository, RulesRepository rulesRepository, StorageService storageService, InitializeParams params, ApplicationEventPublisher eventPublisher, - SeverityModeService severityModeService, ConnectionConfigurationRepository connectionConfigurationRepository) { + SeverityModeService severityModeService) { this(serverApiProvider, configurationRepository, rulesRepository, storageService, eventPublisher, - params.getStandaloneRuleConfigByKey(), severityModeService, connectionConfigurationRepository); + params.getStandaloneRuleConfigByKey(), severityModeService); } RulesService(ServerApiProvider serverApiProvider, ConfigurationRepository configurationRepository, RulesRepository rulesRepository, StorageService storageService, ApplicationEventPublisher eventPublisher, - @Nullable Map standaloneRuleConfigByKey, SeverityModeService severityModeService, - ConnectionConfigurationRepository connectionConfigurationRepository) { + @Nullable Map standaloneRuleConfigByKey, SeverityModeService severityModeService) { this.serverApiProvider = serverApiProvider; this.configurationRepository = configurationRepository; this.rulesRepository = rulesRepository; @@ -117,7 +114,6 @@ public RulesService(ServerApiProvider serverApiProvider, ConfigurationRepository if (standaloneRuleConfigByKey != null) { this.standaloneRuleConfig.putAll(standaloneRuleConfigByKey); } - this.connectionConfigurationRepository = connectionConfigurationRepository; } public EffectiveRuleDetailsDto getEffectiveRuleDetails(String configurationScopeId, String ruleKey, @Nullable String contextKey, @@ -143,11 +139,7 @@ public RuleDetails getRuleDetails(String configurationScopeId, String ruleKey, S public RuleDetails getActiveRuleForBinding(String ruleKey, Binding binding, SonarLintCancelMonitor cancelMonitor) { var connectionId = binding.getConnectionId(); - - var endpointParams = connectionConfigurationRepository.getEndpointParams(connectionId); - if (endpointParams.isEmpty()) { - throw unknownConnection(connectionId); - } + serverApiProvider.getConnectionOrThrow(connectionId); var serverUsesStandardSeverityMode = !severityModeService.isMQRModeForConnection(connectionId); @@ -176,16 +168,16 @@ private Optional findServerActiveRuleInStorage(Binding binding private RuleDetails hydrateDetailsWithServer(String connectionId, ServerActiveRule activeRuleFromStorage, boolean skipCleanCodeTaxonomy, SonarLintCancelMonitor cancelMonitor) { var ruleKey = activeRuleFromStorage.getRuleKey(); var templateKey = activeRuleFromStorage.getTemplateKey(); - var serverApi = serverApiProvider.getServerApiOrThrow(connectionId); + var serverConnection = serverApiProvider.getConnectionOrThrow(connectionId); if (StringUtils.isNotBlank(templateKey)) { var templateRule = rulesRepository.getRule(connectionId, templateKey); if (templateRule.isEmpty()) { throw ruleDefinitionNotFound(templateKey); } - var serverRule = fetchRuleFromServer(connectionId, ruleKey, serverApi, cancelMonitor); + var serverRule = serverConnection.withClientApiAndReturn(serverApi -> fetchRuleFromServer(connectionId, ruleKey, serverApi, cancelMonitor)); return RuleDetails.merging(activeRuleFromStorage, serverRule, templateRule.get(), skipCleanCodeTaxonomy); } else { - var serverRule = fetchRuleFromServer(connectionId, ruleKey, serverApi, cancelMonitor); + var serverRule = serverConnection.withClientApiAndReturn(serverApi -> fetchRuleFromServer(connectionId, ruleKey, serverApi, cancelMonitor)); var ruleDefFromPluginOpt = rulesRepository.getRule(connectionId, ruleKey); return ruleDefFromPluginOpt .map(ruleDefFromPlugin -> RuleDetails.merging(serverRule, ruleDefFromPlugin, skipCleanCodeTaxonomy)) @@ -193,12 +185,6 @@ private RuleDetails hydrateDetailsWithServer(String connectionId, ServerActiveRu } } - @NotNull - private static ResponseErrorException unknownConnection(String connectionId) { - var error = new ResponseError(SonarLintRpcErrorCode.CONNECTION_NOT_FOUND, "Connection with ID '" + connectionId + "' does not exist", connectionId); - return new ResponseErrorException(error); - } - private static ServerRule fetchRuleFromServer(String connectionId, String ruleKey, ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { return serverApi.rules().getRule(ruleKey, cancelMonitor).orElseThrow(() -> ruleNotFoundOnServer(ruleKey, connectionId)); } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/server/event/SonarQubeEventStream.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/server/event/SonarQubeEventStream.java index 9a6f3b350c..213284144e 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/server/event/SonarQubeEventStream.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/server/event/SonarQubeEventStream.java @@ -67,8 +67,9 @@ public synchronized void unsubscribe(String projectKey) { private void attemptSubscription(Set projectKeys) { if (!enabledLanguages.isEmpty()) { - serverApiProvider.getServerApi(connectionId) - .ifPresent(serverApi -> eventStream = serverApi.push().subscribe(projectKeys, enabledLanguages, e -> notifyHandlers(e, eventConsumer))); + serverApiProvider.tryGetConnection(connectionId) + .ifPresent(connection -> eventStream = connection.withClientApiAndReturn(serverApi -> + serverApi.push().subscribe(projectKeys, enabledLanguages, e -> notifyHandlers(e, eventConsumer)))); } } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/smartnotifications/SmartNotifications.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/smartnotifications/SmartNotifications.java index e62d04f488..5578f69d09 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/smartnotifications/SmartNotifications.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/smartnotifications/SmartNotifications.java @@ -101,7 +101,8 @@ private void poll(SonarLintCancelMonitor cancelMonitor) { boundScopeByConnectionAndSonarProject.forEach((connectionId, boundScopesByProject) -> { var connection = connectionRepository.getConnectionById(connectionId); if (connection != null && !connection.isDisableNotifications() && !shouldSkipPolling(connection)) { - serverApiProvider.getServerApi(connectionId).ifPresent(serverApi -> manageNotificationsForConnection(serverApi, boundScopesByProject, connection, cancelMonitor)); + serverApiProvider.withValidConnection(connectionId, serverConnection -> + serverConnection.withClientApi(serverApi -> manageNotificationsForConnection(serverApi, boundScopesByProject, connection, cancelMonitor))); } }); } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/HotspotSynchronizationService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/HotspotSynchronizationService.java index 029573922c..01f043c59f 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/HotspotSynchronizationService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/HotspotSynchronizationService.java @@ -73,8 +73,9 @@ private static Version getSonarServerVersion(ServerApi serverApi, ConnectionStor } public void fetchProjectHotspots(Binding binding, String activeBranch, SonarLintCancelMonitor cancelMonitor) { - serverApiProvider.getServerApi(binding.getConnectionId()) - .ifPresent(serverApi -> downloadAllServerHotspots(binding.getConnectionId(), serverApi, binding.getSonarProjectKey(), activeBranch, cancelMonitor)); + serverApiProvider.tryGetConnection(binding.getConnectionId()) + .ifPresent(connection -> connection.withClientApi(serverApi -> + downloadAllServerHotspots(binding.getConnectionId(), serverApi, binding.getSonarProjectKey(), activeBranch, cancelMonitor))); } private void downloadAllServerHotspots(String connectionId, ServerApi serverApi, String projectKey, String branchName, SonarLintCancelMonitor cancelMonitor) { @@ -87,8 +88,9 @@ private void downloadAllServerHotspots(String connectionId, ServerApi serverApi, } public void fetchFileHotspots(Binding binding, String activeBranch, Path serverFilePath, SonarLintCancelMonitor cancelMonitor) { - serverApiProvider.getServerApi(binding.getConnectionId()) - .ifPresent(serverApi -> downloadAllServerHotspotsForFile(binding.getConnectionId(), serverApi, binding.getSonarProjectKey(), serverFilePath, activeBranch, cancelMonitor)); + serverApiProvider.tryGetConnection(binding.getConnectionId()) + .ifPresent(connection -> connection.withClientApi(serverApi -> + downloadAllServerHotspotsForFile(binding.getConnectionId(), serverApi, binding.getSonarProjectKey(), serverFilePath, activeBranch, cancelMonitor))); } private void downloadAllServerHotspotsForFile(String connectionId, ServerApi serverApi, String projectKey, Path serverRelativeFilePath, String branchName, diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/IssueSynchronizationService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/IssueSynchronizationService.java index 82842b7bb0..d00e10551e 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/IssueSynchronizationService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/IssueSynchronizationService.java @@ -65,8 +65,9 @@ public void syncServerIssuesForProject(ServerApi serverApi, String connectionId, } public void fetchProjectIssues(Binding binding, String activeBranch, SonarLintCancelMonitor cancelMonitor) { - serverApiProvider.getServerApi(binding.getConnectionId()) - .ifPresent(serverApi -> downloadServerIssuesForProject(binding.getConnectionId(), serverApi, binding.getSonarProjectKey(), activeBranch, cancelMonitor)); + serverApiProvider.tryGetConnection(binding.getConnectionId()) + .ifPresent( connection -> connection.withClientApi(serverApi -> + downloadServerIssuesForProject(binding.getConnectionId(), serverApi, binding.getSonarProjectKey(), activeBranch, cancelMonitor))); } private void downloadServerIssuesForProject(String connectionId, ServerApi serverApi, String projectKey, String branchName, SonarLintCancelMonitor cancelMonitor) { @@ -78,8 +79,9 @@ private void downloadServerIssuesForProject(String connectionId, ServerApi serve } public void fetchFileIssues(Binding binding, Path serverFileRelativePath, String activeBranch, SonarLintCancelMonitor cancelMonitor) { - serverApiProvider.getServerApi(binding.getConnectionId()) - .ifPresent(serverApi -> downloadServerIssuesForFile(binding.getConnectionId(), serverApi, binding.getSonarProjectKey(), serverFileRelativePath, activeBranch, cancelMonitor)); + serverApiProvider.tryGetConnection(binding.getConnectionId()) + .ifPresent(connection -> connection.withClientApi((serverApi -> + downloadServerIssuesForFile(binding.getConnectionId(), serverApi, binding.getSonarProjectKey(), serverFileRelativePath, activeBranch, cancelMonitor)))); } public void downloadServerIssuesForFile(String connectionId, ServerApi serverApi, String projectKey, Path serverFileRelativePath, String branchName, diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/LastWebApiErrorNotificationService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/LastWebApiErrorNotificationService.java new file mode 100644 index 0000000000..73f7ef9ed9 --- /dev/null +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/LastWebApiErrorNotificationService.java @@ -0,0 +1,45 @@ +/* + * SonarLint Core - Implementation + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarsource.sonarlint.core.sync; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import javax.annotation.CheckForNull; +import org.sonarsource.sonarlint.core.storage.StorageService; + +public class LastWebApiErrorNotificationService { + private final StorageService storage; + + public LastWebApiErrorNotificationService(StorageService storage) { + this.storage = storage; + } + + public void setLastWebApiErrorNotification(String connectionId, ZonedDateTime lastErrorNotificationTime) { + storage.connection(connectionId).webApiErrorNotifications().store(lastErrorNotificationTime.toInstant().toEpochMilli()); + } + + @CheckForNull + public ZonedDateTime getLastWebApiErrorNotification(String connectionId) { + return storage.getStorageFacade().connection(connectionId) + .webApiErrorNotifications().readLastWebApiErrorNotification() + .map(aLong -> ZonedDateTime.ofInstant(Instant.ofEpochMilli(aLong), ZoneId.systemDefault())).orElse(null); + } +} diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SonarProjectBranchesSynchronizationService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SonarProjectBranchesSynchronizationService.java index a753098654..719663ef91 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SonarProjectBranchesSynchronizationService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SonarProjectBranchesSynchronizationService.java @@ -52,7 +52,7 @@ public SonarProjectBranchesSynchronizationService(StorageService storageService, } public void sync(String connectionId, String sonarProjectKey, SonarLintCancelMonitor cancelMonitor) { - serverApiProvider.getServerApi(connectionId).ifPresent(serverApi -> { + serverApiProvider.tryGetConnection(connectionId).ifPresent( connection -> connection.withClientApi(serverApi -> { var branchesStorage = storageService.getStorageFacade().connection(connectionId).project(sonarProjectKey).branches(); Optional oldBranches = Optional.empty(); if (branchesStorage.exists()) { @@ -64,7 +64,7 @@ public void sync(String connectionId, String sonarProjectKey, SonarLintCancelMon LOG.debug("Project branches changed for project '{}'", sonarProjectKey); eventPublisher.publishEvent(new SonarProjectBranchesChangedEvent(connectionId, sonarProjectKey)); } - }); + })); } public ProjectBranches getProjectBranches(ServerApi serverApi, String projectKey, SonarLintCancelMonitor cancelMonitor) { @@ -81,8 +81,8 @@ public String findMainBranch(String connectionId, String projectKey, SonarLintCa var storedBranches = branchesStorage.read(); return storedBranches.getMainBranchName(); } else { - var serverApi = serverApiProvider.getServerApiOrThrow(connectionId); - var branches = getProjectBranches(serverApi, projectKey, cancelMonitor); + var branches = serverApiProvider.getConnectionOrThrow(connectionId) + .withClientApiAndReturn(serverApi -> getProjectBranches(serverApi, projectKey, cancelMonitor)); return branches.getMainBranchName(); } } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SynchronizationService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SynchronizationService.java index 0bf22af8b2..c77eeb3767 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SynchronizationService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SynchronizationService.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -33,16 +34,18 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Named; import javax.inject.Singleton; -import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.ServerApiProvider; import org.sonarsource.sonarlint.core.branch.MatchedSonarProjectBranchChangedEvent; import org.sonarsource.sonarlint.core.branch.SonarProjectBranchTrackingService; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.BoundScope; +import org.sonarsource.sonarlint.core.commons.Version; +import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ExecutorServiceShutdownWatchable; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; @@ -59,7 +62,10 @@ import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.sync.DidSynchronizeConfigurationScopeParams; import org.sonarsource.sonarlint.core.serverapi.ServerApi; -import org.sonarsource.sonarlint.core.serverconnection.ServerConnection; +import org.sonarsource.sonarlint.core.serverapi.exception.ForbiddenException; +import org.sonarsource.sonarlint.core.serverapi.exception.UnauthorizedException; +import org.sonarsource.sonarlint.core.serverconnection.LocalStorageSynchronizer; +import org.sonarsource.sonarlint.core.serverconnection.ServerInfoSynchronizer; import org.sonarsource.sonarlint.core.serverconnection.SonarServerSettingsChangedEvent; import org.sonarsource.sonarlint.core.storage.StorageService; import org.springframework.context.ApplicationEventPublisher; @@ -178,14 +184,14 @@ private void synchronizeProjectsOfTheSameConnection(String connectionId, Map { + serverApiProvider.tryGetConnection(connectionId).ifPresent(connection -> connection.withClientApi(serverApi -> { var subProgressGap = progressGap / boundScopeBySonarProject.size(); var subProgress = progress; for (var entry : boundScopeBySonarProject.entrySet()) { synchronizeProjectWithProgress(serverApi, connectionId, entry.getKey(), entry.getValue(), notifier, cancelMonitor, synchronizedConfScopeIds, subProgress); subProgress += subProgressGap; } - }); + })); } private void synchronizeProjectWithProgress(ServerApi serverApi, String connectionId, String sonarProjectKey, Collection boundScopes, ProgressNotifier notifier, @@ -207,10 +213,9 @@ private void synchronizeProjectWithProgress(ServerApi serverApi, String connecti })); } - @NotNull - public ServerConnection getServerConnection(String connectionId, ServerApi serverApi) { - return new ServerConnection(storageService.getStorageFacade(), connectionId, serverApi.isSonarCloud(), - languageSupportRepository.getEnabledLanguagesInConnectedMode(), connectedModeEmbeddedPluginKeys); + public Version readOrSynchronizeServerVersion(String connectionId, ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { + var serverInfoSynchronizer = new ServerInfoSynchronizer(storageService.getStorageFacade().connection(connectionId)); + return serverInfoSynchronizer.readOrSynchronizeServerInfo(serverApi, cancelMonitor).getVersion(); } @EventListener @@ -291,8 +296,9 @@ public void onConnectionCredentialsChanged(ConnectionCredentialsChangedEvent eve private void synchronizeConnectionAndProjectsIfNeededAsync(String connectionId, Collection boundScopes) { var cancelMonitor = new SonarLintCancelMonitor(); cancelMonitor.watchForShutdown(scheduledSynchronizer); - scheduledSynchronizer.submit(() -> serverApiProvider.getServerApi(connectionId) - .ifPresent(serverApi -> synchronizeConnectionAndProjectsIfNeededSync(connectionId, serverApi, boundScopes, cancelMonitor))); + scheduledSynchronizer.submit(() -> serverApiProvider.tryGetConnection(connectionId) + .ifPresent(connection -> connection.withClientApi(serverApi -> + synchronizeConnectionAndProjectsIfNeededSync(connectionId, serverApi, boundScopes, cancelMonitor)))); } private void synchronizeConnectionAndProjectsIfNeededSync(String connectionId, ServerApi serverApi, Collection boundScopes, SonarLintCancelMonitor cancelMonitor) { @@ -303,10 +309,14 @@ private void synchronizeConnectionAndProjectsIfNeededSync(String connectionId, S scopesToSync.forEach(scope -> scopeSynchronizationTimestampRepository.setLastSynchronizationTimestampToNow(scope.getConfigScopeId())); // We will already trigger a sync of the project storage so we can temporarily ignore branch changed event for these config scopes ignoreBranchEventForScopes.addAll(scopesToSync.stream().map(BoundScope::getConfigScopeId).collect(toSet())); - var serverConnection = getServerConnection(connectionId, serverApi); + var enabledLanguagesToSync = languageSupportRepository.getEnabledLanguagesInConnectedMode().stream() + .filter(SonarLanguage::shouldSyncInConnectedMode).collect(Collectors.toCollection(LinkedHashSet::new)); + var storage = storageService.getStorageFacade().connection(connectionId); + var serverInfoSynchronizer = new ServerInfoSynchronizer(storage); + var storageSynchronizer = new LocalStorageSynchronizer(enabledLanguagesToSync, connectedModeEmbeddedPluginKeys, serverInfoSynchronizer, storage); try { LOG.debug("Synchronizing storage of connection '{}'", connectionId); - var summary = serverConnection.sync(serverApi, cancelMonitor); + var summary = storageSynchronizer.synchronizeServerInfosAndPlugins(serverApi, cancelMonitor); if (summary.anyPluginSynchronized()) { // TODO re-review this solution before merging pluginsRepository.unload(connectionId); @@ -319,7 +329,7 @@ private void synchronizeConnectionAndProjectsIfNeededSync(String connectionId, S scopesPerProjectKey.forEach((projectKey, configScopeIds) -> { bindingSynchronizationTimestampRepository.setLastSynchronizationTimestampToNow(new Binding(connectionId, projectKey)); LOG.debug("Synchronizing storage of Sonar project '{}' for connection '{}'", projectKey, connectionId); - var analyzerConfigUpdateSummary = serverConnection.sync(serverApi, projectKey, cancelMonitor); + var analyzerConfigUpdateSummary = storageSynchronizer.synchronizeAnalyzerConfig(serverApi, projectKey, cancelMonitor); // XXX we might want to group those 2 events under one if (!analyzerConfigUpdateSummary.getUpdatedSettingsValueByKey().isEmpty()) { applicationEventPublisher.publishEvent( @@ -334,6 +344,9 @@ private void synchronizeConnectionAndProjectsIfNeededSync(String connectionId, S cancelMonitor); } catch (Exception e) { LOG.error("Error during synchronization", e); + if (e instanceof UnauthorizedException || e instanceof ForbiddenException) { + throw e; + } } finally { ignoreBranchEventForScopes.removeAll(scopesToSync.stream().map(BoundScope::getConfigScopeId).collect(toSet())); } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/TaintSynchronizationService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/TaintSynchronizationService.java index bbd49c5d39..eeda4cc9f2 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/TaintSynchronizationService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/TaintSynchronizationService.java @@ -66,13 +66,13 @@ public TaintSynchronizationService(ConfigurationRepository configurationReposito } public void synchronizeTaintVulnerabilities(String connectionId, String projectKey, SonarLintCancelMonitor cancelMonitor) { - serverApiProvider.getServerApi(connectionId).ifPresent(serverApi -> { + serverApiProvider.tryGetConnection(connectionId).ifPresent( connection -> connection.withClientApi(serverApi -> { var allScopes = configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, projectKey); var allScopesByOptBranch = allScopes.stream() .collect(groupingBy(b -> branchTrackingService.awaitEffectiveSonarProjectBranch(b.getConfigScopeId()))); allScopesByOptBranch .forEach((branchNameOpt, scopes) -> branchNameOpt.ifPresent(branchName -> synchronizeTaintVulnerabilities(serverApi, connectionId, projectKey, branchName, cancelMonitor))); - }); + })); } public void synchronizeTaintVulnerabilities(ServerApi serverApi, String connectionId, String projectKey, String branch, SonarLintCancelMonitor cancelMonitor) { diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/ServerApiProviderTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/ServerApiProviderTests.java index 599ec9dee1..ace7878677 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/ServerApiProviderTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/ServerApiProviderTests.java @@ -30,7 +30,9 @@ import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; +import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.serverapi.EndpointParams; +import org.sonarsource.sonarlint.core.storage.StorageService; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -43,7 +45,10 @@ class ServerApiProviderTests { private final ConnectionConfigurationRepository connectionRepository = mock(ConnectionConfigurationRepository.class); private final ConnectionAwareHttpClientProvider awareHttpClientProvider = mock(ConnectionAwareHttpClientProvider.class); private final HttpClientProvider httpClientProvider = mock(HttpClientProvider.class); - private final ServerApiProvider underTest = new ServerApiProvider(connectionRepository, awareHttpClientProvider, httpClientProvider, SonarCloudActiveEnvironment.prod()); + private final SonarLintRpcClient client = mock(SonarLintRpcClient.class); + private final StorageService storageService = mock(StorageService.class); + private final ServerApiProvider underTest = new ServerApiProvider(connectionRepository, awareHttpClientProvider, httpClientProvider, + SonarCloudActiveEnvironment.prod(), storageService, client); @Test void getServerApi_for_sonarqube() { diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/SonarProjectsCacheTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/SonarProjectsCacheTests.java index e1ab673fdc..52345bf2c0 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/SonarProjectsCacheTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/SonarProjectsCacheTests.java @@ -19,6 +19,7 @@ */ package org.sonarsource.sonarlint.core; +import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; @@ -27,10 +28,12 @@ import org.mockito.Mockito; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; +import org.sonarsource.sonarlint.core.connection.ServerConnection; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationRemovedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationUpdatedEvent; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.component.ServerProject; +import org.sonarsource.sonarlint.core.sync.LastWebApiErrorNotificationService; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -88,7 +91,11 @@ public String getName() { @BeforeEach public void setup() { + var lastWebApiErrorNotificationService = mock(LastWebApiErrorNotificationService.class); when(serverApiProvider.getServerApi(SQ_1)).thenReturn(Optional.of(serverApi)); + when(lastWebApiErrorNotificationService.getLastWebApiErrorNotification(SQ_1)).thenReturn(ZonedDateTime.now()); + var serverConnection = new ServerConnection(SQ_1, serverApi, lastWebApiErrorNotificationService, null); + when(serverApiProvider.tryGetConnection(SQ_1)).thenReturn(Optional.of(serverConnection)); } @Test diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelperTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelperTests.java index 0ee533bd20..b7df6e1ab1 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelperTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelperTests.java @@ -19,6 +19,7 @@ */ package org.sonarsource.sonarlint.core; +import java.time.ZonedDateTime; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.BeforeEach; @@ -29,6 +30,7 @@ import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; +import org.sonarsource.sonarlint.core.connection.ServerConnection; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopesAddedEvent; import org.sonarsource.sonarlint.core.repository.config.BindingConfiguration; @@ -39,8 +41,8 @@ import org.sonarsource.sonarlint.core.repository.connection.SonarQubeConnectionConfiguration; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.serverapi.ServerApi; -import org.sonarsource.sonarlint.core.serverconnection.ServerConnection; import org.sonarsource.sonarlint.core.serverconnection.VersionUtils; +import org.sonarsource.sonarlint.core.sync.LastWebApiErrorNotificationService; import org.sonarsource.sonarlint.core.sync.SynchronizationService; import static org.assertj.core.api.Assertions.assertThat; @@ -68,6 +70,7 @@ class VersionSoonUnsupportedHelperTests { private final SonarLintRpcClient client = mock(SonarLintRpcClient.class); private final ServerApiProvider serverApiProvider = mock(ServerApiProvider.class); private final SynchronizationService synchronizationService = mock(SynchronizationService.class); + private final LastWebApiErrorNotificationService lastWebApiErrorNotificationService = mock(LastWebApiErrorNotificationService.class); private ConfigurationRepository configRepository; private ConnectionConfigurationRepository connectionRepository; @@ -77,6 +80,7 @@ class VersionSoonUnsupportedHelperTests { void init() { configRepository = new ConfigurationRepository(); connectionRepository = new ConnectionConfigurationRepository(); + when(lastWebApiErrorNotificationService.getLastWebApiErrorNotification(any())).thenReturn(ZonedDateTime.now()); underTest = new VersionSoonUnsupportedHelper(client, configRepository, serverApiProvider, connectionRepository, synchronizationService); } @@ -87,10 +91,9 @@ void should_trigger_notification_when_new_binding_to_previous_lts_detected_on_co configRepository.addOrReplace(new ConfigurationScope(CONFIG_SCOPE_ID_2, null, false, ""), bindingConfiguration); connectionRepository.addOrReplace(SQ_CONNECTION); var serverApi = mock(ServerApi.class); - when(serverApiProvider.getServerApi(SQ_CONNECTION_ID)).thenReturn(Optional.of(serverApi)); - var serverConnection = mock(ServerConnection.class); - when(serverConnection.readOrSynchronizeServerVersion(eq(serverApi), any(SonarLintCancelMonitor.class))).thenReturn(VersionUtils.getMinimalSupportedVersion()); - when(synchronizationService.getServerConnection(SQ_CONNECTION_ID, serverApi)).thenReturn(serverConnection); + when(serverApiProvider.tryGetConnection(SQ_CONNECTION_ID)).thenReturn(Optional.of(new ServerConnection(SQ_CONNECTION_ID, serverApi, lastWebApiErrorNotificationService, null))); + when(synchronizationService.readOrSynchronizeServerVersion(eq(SQ_CONNECTION_ID), eq(serverApi), any(SonarLintCancelMonitor.class))).thenReturn(VersionUtils.getMinimalSupportedVersion()); + underTest.configurationScopesAdded(new ConfigurationScopesAddedEvent(Set.of(CONFIG_SCOPE_ID, CONFIG_SCOPE_ID_2))); @@ -108,14 +111,10 @@ void should_trigger_multiple_notification_when_new_bindings_to_previous_lts_dete connectionRepository.addOrReplace(SQ_CONNECTION_2); var serverApi = mock(ServerApi.class); var serverApi2 = mock(ServerApi.class); - when(serverApiProvider.getServerApi(SQ_CONNECTION_ID)).thenReturn(Optional.of(serverApi)); - when(serverApiProvider.getServerApi(SQ_CONNECTION_ID_2)).thenReturn(Optional.of(serverApi2)); - var serverConnection = mock(ServerConnection.class); - when(serverConnection.readOrSynchronizeServerVersion(eq(serverApi), any(SonarLintCancelMonitor.class))).thenReturn(VersionUtils.getMinimalSupportedVersion()); - var serverConnection2 = mock(ServerConnection.class); - when(serverConnection2.readOrSynchronizeServerVersion(eq(serverApi2), any(SonarLintCancelMonitor.class))).thenReturn(Version.create(VersionUtils.getMinimalSupportedVersion() + ".9")); - when(synchronizationService.getServerConnection(SQ_CONNECTION_ID, serverApi)).thenReturn(serverConnection); - when(synchronizationService.getServerConnection(SQ_CONNECTION_ID_2, serverApi2)).thenReturn(serverConnection2); + when(serverApiProvider.tryGetConnection(SQ_CONNECTION_ID)).thenReturn(Optional.of(new ServerConnection(SQ_CONNECTION_ID, serverApi, lastWebApiErrorNotificationService, null))); + when(serverApiProvider.tryGetConnection(CONFIG_SCOPE_ID_2)).thenReturn(Optional.of(new ServerConnection(CONFIG_SCOPE_ID_2, serverApi2, lastWebApiErrorNotificationService, null))); + when(synchronizationService.readOrSynchronizeServerVersion(eq(SQ_CONNECTION_ID), eq(serverApi), any(SonarLintCancelMonitor.class))).thenReturn(VersionUtils.getMinimalSupportedVersion()); + when(synchronizationService.readOrSynchronizeServerVersion(eq(SQ_CONNECTION_ID_2), eq(serverApi2), any(SonarLintCancelMonitor.class))).thenReturn(Version.create(VersionUtils.getMinimalSupportedVersion() + ".9")); underTest.configurationScopesAdded(new ConfigurationScopesAddedEvent(Set.of(CONFIG_SCOPE_ID, CONFIG_SCOPE_ID_2))); @@ -137,10 +136,9 @@ void should_not_trigger_notification_when_config_scope_has_no_effective_binding( void should_trigger_notification_when_new_binding_to_previous_lts_detected() { connectionRepository.addOrReplace(SQ_CONNECTION); var serverApi = mock(ServerApi.class); - when(serverApiProvider.getServerApi(SQ_CONNECTION_ID)).thenReturn(Optional.of(serverApi)); - var serverConnection = mock(ServerConnection.class); - when(serverConnection.readOrSynchronizeServerVersion(eq(serverApi), any(SonarLintCancelMonitor.class))).thenReturn(VersionUtils.getMinimalSupportedVersion()); - when(synchronizationService.getServerConnection(SQ_CONNECTION_ID, serverApi)).thenReturn(serverConnection); + when(serverApiProvider.tryGetConnection(SQ_CONNECTION_ID)).thenReturn(Optional.of(new ServerConnection(SQ_CONNECTION_ID, serverApi, lastWebApiErrorNotificationService, null))); + when(synchronizationService.readOrSynchronizeServerVersion(eq(SQ_CONNECTION_ID), eq(serverApi), any(SonarLintCancelMonitor.class))) + .thenReturn(VersionUtils.getMinimalSupportedVersion()); underTest.bindingConfigChanged(new BindingConfigChangedEvent(CONFIG_SCOPE_ID, null, new BindingConfiguration(SQ_CONNECTION_ID, "", false))); @@ -153,10 +151,8 @@ void should_trigger_notification_when_new_binding_to_previous_lts_detected() { void should_trigger_once_when_same_binding_to_previous_lts_detected_twice() { connectionRepository.addOrReplace(SQ_CONNECTION); var serverApi = mock(ServerApi.class); - when(serverApiProvider.getServerApi(SQ_CONNECTION_ID)).thenReturn(Optional.of(serverApi)); - var serverConnection = mock(ServerConnection.class); - when(serverConnection.readOrSynchronizeServerVersion(eq(serverApi), any(SonarLintCancelMonitor.class))).thenReturn(VersionUtils.getMinimalSupportedVersion()); - when(synchronizationService.getServerConnection(SQ_CONNECTION_ID, serverApi)).thenReturn(serverConnection); + when(serverApiProvider.tryGetConnection(SQ_CONNECTION_ID)).thenReturn(Optional.of(new ServerConnection(SQ_CONNECTION_ID, serverApi, lastWebApiErrorNotificationService, null))); + when(synchronizationService.readOrSynchronizeServerVersion(eq(SQ_CONNECTION_ID), eq(serverApi), any(SonarLintCancelMonitor.class))).thenReturn(VersionUtils.getMinimalSupportedVersion()); underTest.bindingConfigChanged(new BindingConfigChangedEvent(CONFIG_SCOPE_ID, null, new BindingConfiguration(SQ_CONNECTION_ID, "", false))); @@ -171,10 +167,8 @@ void should_trigger_once_when_same_binding_to_previous_lts_detected_twice() { void should_trigger_notification_when_new_binding_to_in_between_lts_detected() { connectionRepository.addOrReplace(SQ_CONNECTION); var serverApi = mock(ServerApi.class); - when(serverApiProvider.getServerApi(SQ_CONNECTION_ID)).thenReturn(Optional.of(serverApi)); - var serverConnection = mock(ServerConnection.class); - when(serverConnection.readOrSynchronizeServerVersion(eq(serverApi), any(SonarLintCancelMonitor.class))).thenReturn(Version.create(VersionUtils.getMinimalSupportedVersion().getName() + ".9")); - when(synchronizationService.getServerConnection(SQ_CONNECTION_ID, serverApi)).thenReturn(serverConnection); + when(serverApiProvider.tryGetConnection(SQ_CONNECTION_ID)).thenReturn(Optional.of(new ServerConnection(SQ_CONNECTION_ID, serverApi, lastWebApiErrorNotificationService, null))); + when(synchronizationService.readOrSynchronizeServerVersion(eq(SQ_CONNECTION_ID), eq(serverApi), any(SonarLintCancelMonitor.class))).thenReturn(Version.create(VersionUtils.getMinimalSupportedVersion().getName() + ".9")); underTest.bindingConfigChanged(new BindingConfigChangedEvent(CONFIG_SCOPE_ID, null, new BindingConfiguration(SQ_CONNECTION_ID, "", false))); @@ -187,10 +181,8 @@ void should_trigger_notification_when_new_binding_to_in_between_lts_detected() { void should_not_trigger_notification_when_new_binding_to_current_lts_detected() { connectionRepository.addOrReplace(SQ_CONNECTION); var serverApi = mock(ServerApi.class); - when(serverApiProvider.getServerApi(SQ_CONNECTION_ID)).thenReturn(Optional.of(serverApi)); - var serverConnection = mock(ServerConnection.class); - when(serverConnection.readOrSynchronizeServerVersion(eq(serverApi), any(SonarLintCancelMonitor.class))).thenReturn(VersionUtils.getCurrentLts()); - when(synchronizationService.getServerConnection(SQ_CONNECTION_ID, serverApi)).thenReturn(serverConnection); + when(serverApiProvider.tryGetConnection(SQ_CONNECTION_ID)).thenReturn(Optional.of(new ServerConnection(SQ_CONNECTION_ID, serverApi, lastWebApiErrorNotificationService, null))); + when(synchronizationService.readOrSynchronizeServerVersion(eq(SQ_CONNECTION_ID), eq(serverApi), any(SonarLintCancelMonitor.class))).thenReturn(VersionUtils.getCurrentLts()); underTest.bindingConfigChanged(new BindingConfigChangedEvent(CONFIG_SCOPE_ID, null, new BindingConfiguration(SQ_CONNECTION_ID, "", false))); diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandlerTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandlerTests.java index f1e4672c91..3894a97f03 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandlerTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ShowIssueRequestHandlerTests.java @@ -24,6 +24,7 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; import java.util.Set; @@ -48,6 +49,7 @@ import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; +import org.sonarsource.sonarlint.core.connection.ServerConnection; import org.sonarsource.sonarlint.core.file.FilePathTranslation; import org.sonarsource.sonarlint.core.file.PathTranslationService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; @@ -67,6 +69,7 @@ import org.sonarsource.sonarlint.core.serverconnection.ProjectBranchesStorage; import org.sonarsource.sonarlint.core.serverconnection.SonarProjectStorage; import org.sonarsource.sonarlint.core.storage.StorageService; +import org.sonarsource.sonarlint.core.sync.LastWebApiErrorNotificationService; import org.sonarsource.sonarlint.core.sync.SonarProjectBranchesSynchronizationService; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import org.sonarsource.sonarlint.core.usertoken.UserTokenService; @@ -75,6 +78,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -95,6 +99,7 @@ class ShowIssueRequestHandlerTests { private ProjectBranchesStorage branchesStorage; private IssueApi issueApi; private TelemetryService telemetryService; + private final LastWebApiErrorNotificationService lastWebApiErrorNotificationService = mock(LastWebApiErrorNotificationService.class); @BeforeEach void setup() { @@ -112,15 +117,17 @@ void setup() { issueApi = mock(IssueApi.class); var serverApi = mock(ServerApi.class); when(serverApi.issue()).thenReturn(issueApi); + when(lastWebApiErrorNotificationService.getLastWebApiErrorNotification(anyString())).thenReturn(ZonedDateTime.now()); + var connection = new ServerConnection("connectionId", serverApi, lastWebApiErrorNotificationService, sonarLintRpcClient); var serverApiProvider = mock(ServerApiProvider.class); - when(serverApiProvider.getServerApiOrThrow(any())).thenReturn(serverApi); + when(serverApiProvider.tryGetConnection(any())).thenReturn(Optional.of(connection)); + when(serverApiProvider.getConnectionOrThrow(any())).thenReturn(connection); when(serverApiProvider.getServerApi(any())).thenReturn(Optional.of(serverApi)); branchesStorage = mock(ProjectBranchesStorage.class); var storageService = mock(StorageService.class); var sonarStorage = mock(SonarProjectStorage.class); var eventPublisher = mock(ApplicationEventPublisher.class); - var sonarProjectBranchesSynchronizationService = spy(new SonarProjectBranchesSynchronizationService(storageService, serverApiProvider - , eventPublisher)); + var sonarProjectBranchesSynchronizationService = spy(new SonarProjectBranchesSynchronizationService(storageService, serverApiProvider, eventPublisher)); doReturn(new ProjectBranches(Set.of(), "main")).when(sonarProjectBranchesSynchronizationService).getProjectBranches(any(), any(), any()); when(storageService.binding(any())).thenReturn(sonarStorage); diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/file/ServerFilePathsProviderTest.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/file/ServerFilePathsProviderTest.java index 85b1228d7f..f88642ae84 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/file/ServerFilePathsProviderTest.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/file/ServerFilePathsProviderTest.java @@ -27,6 +27,7 @@ import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; +import java.time.ZonedDateTime; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -41,8 +42,10 @@ import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; +import org.sonarsource.sonarlint.core.connection.ServerConnection; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.component.ComponentApi; +import org.sonarsource.sonarlint.core.sync.LastWebApiErrorNotificationService; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; @@ -68,6 +71,7 @@ class ServerFilePathsProviderTest { private final SonarLintCancelMonitor cancelMonitor =mock(SonarLintCancelMonitor.class); private final ComponentApi componentApi_A = mock(ComponentApi.class); private final ComponentApi componentApi_B = mock(ComponentApi.class); + private final LastWebApiErrorNotificationService lastWebApiErrorNotificationService = mock(LastWebApiErrorNotificationService.class); private ServerFilePathsProvider underTest; @BeforeEach @@ -75,8 +79,13 @@ void before(@TempDir Path storageDir) throws IOException { cacheDirectory = storageDir.resolve("cache"); Files.createDirectories(cacheDirectory); + when(lastWebApiErrorNotificationService.getLastWebApiErrorNotification(anyString())).thenReturn(ZonedDateTime.now()); when(serverApiProvider.getServerApi(CONNECTION_A)).thenReturn(Optional.of(serverApi_A)); when(serverApiProvider.getServerApi(CONNECTION_B)).thenReturn(Optional.of(serverApi_B)); + var serverConnectionA = new ServerConnection(CONNECTION_A, serverApi_A, lastWebApiErrorNotificationService, null); + var serverConnectionB = new ServerConnection(CONNECTION_B, serverApi_B, lastWebApiErrorNotificationService, null); + when(serverApiProvider.tryGetConnection(CONNECTION_A)).thenReturn(Optional.of(serverConnectionA)); + when(serverApiProvider.tryGetConnection(CONNECTION_B)).thenReturn(Optional.of(serverConnectionB)); when(serverApi_A.component()).thenReturn(componentApi_A); when(serverApi_B.component()).thenReturn(componentApi_B); mockServerFilePaths(componentApi_A, "pathA", "pathB"); diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/rules/RulesServiceTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/rules/RulesServiceTests.java index 85310938f7..7c91f162b2 100644 --- a/backend/core/src/test/java/org/sonarsource/sonarlint/core/rules/RulesServiceTests.java +++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/rules/RulesServiceTests.java @@ -26,7 +26,6 @@ import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; -import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.rules.RulesRepository; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleDefinitionDto; import org.sonarsource.sonarlint.core.serverapi.push.parsing.common.ImpactPayload; @@ -42,20 +41,18 @@ class RulesServiceTests { private RulesRepository rulesRepository; private RulesExtractionHelper extractionHelper; private ConfigurationRepository configurationRepository; - private ConnectionConfigurationRepository connectionConfigurationRepository; @BeforeEach void prepare() { extractionHelper = mock(RulesExtractionHelper.class); configurationRepository = mock(ConfigurationRepository.class); - connectionConfigurationRepository = mock(ConnectionConfigurationRepository.class); rulesRepository = new RulesRepository(extractionHelper, configurationRepository); } @Test void it_should_return_all_embedded_rules_from_the_repository() { when(extractionHelper.extractEmbeddedRules()).thenReturn(List.of(aRule())); - var rulesService = new RulesService(null, null, rulesRepository, null, null, Map.of(), null, connectionConfigurationRepository); + var rulesService = new RulesService(null, null, rulesRepository, null, null, Map.of(), null); var embeddedRules = rulesService.listAllStandaloneRulesDefinitions().values(); diff --git a/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/ServerApiHelper.java b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/ServerApiHelper.java index 9c93dd8230..3cda3497ec 100644 --- a/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/ServerApiHelper.java +++ b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/ServerApiHelper.java @@ -43,8 +43,10 @@ import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.http.HttpClient; import org.sonarsource.sonarlint.core.http.HttpConnectionListener; +import org.sonarsource.sonarlint.core.serverapi.exception.ForbiddenException; import org.sonarsource.sonarlint.core.serverapi.exception.NotFoundException; import org.sonarsource.sonarlint.core.serverapi.exception.ServerErrorException; +import org.sonarsource.sonarlint.core.serverapi.exception.UnauthorizedException; /** * Wrapper around HttpClient to avoid repetitive code, like support of pagination, and log timing of requests @@ -138,11 +140,11 @@ public static String concat(String baseUrl, String relativePath) { public static RuntimeException handleError(HttpClient.Response toBeClosed) { try (var failedResponse = toBeClosed) { if (failedResponse.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { - return new IllegalStateException("Not authorized. Please check server credentials."); + return new UnauthorizedException("Not authorized. Please check server credentials."); } if (failedResponse.code() == HttpURLConnection.HTTP_FORBIDDEN) { // Details are in response content - return new IllegalStateException(tryParseAsJsonError(failedResponse)); + return new ForbiddenException(tryParseAsJsonError(failedResponse)); } if (failedResponse.code() == HttpURLConnection.HTTP_NOT_FOUND) { return new NotFoundException(formatHttpFailedResponse(failedResponse, null)); @@ -169,6 +171,9 @@ private static String tryParseAsJsonError(HttpClient.Response response) { } var obj = JsonParser.parseString(content).getAsJsonObject(); var errors = obj.getAsJsonArray("errors"); + if (errors == null) { + return null; + } List errorMessages = new ArrayList<>(); for (JsonElement e : errors) { errorMessages.add(e.getAsJsonObject().get("msg").getAsString()); diff --git a/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/ForbiddenException.java b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/ForbiddenException.java new file mode 100644 index 0000000000..361ed3108a --- /dev/null +++ b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/ForbiddenException.java @@ -0,0 +1,26 @@ +/* + * SonarLint Core - Server API + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarsource.sonarlint.core.serverapi.exception; + +public class ForbiddenException extends RuntimeException { + public ForbiddenException(String message) { + super(message); + } +} diff --git a/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/UnauthorizedException.java b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/UnauthorizedException.java new file mode 100644 index 0000000000..02fc018646 --- /dev/null +++ b/backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/UnauthorizedException.java @@ -0,0 +1,26 @@ +/* + * SonarLint Core - Server API + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarsource.sonarlint.core.serverapi.exception; + +public class UnauthorizedException extends RuntimeException { + public UnauthorizedException(String message) { + super(message); + } +} diff --git a/backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ConnectionStorage.java b/backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ConnectionStorage.java index 799a8143cb..e83fad2da7 100644 --- a/backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ConnectionStorage.java +++ b/backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ConnectionStorage.java @@ -25,6 +25,7 @@ import org.sonarsource.sonarlint.core.serverconnection.storage.PluginsStorage; import org.sonarsource.sonarlint.core.serverconnection.storage.ServerInfoStorage; import org.sonarsource.sonarlint.core.serverconnection.storage.ServerIssueStoresManager; +import org.sonarsource.sonarlint.core.serverconnection.storage.WebApiErrorNotificationsStorage; import static org.sonarsource.sonarlint.core.serverconnection.storage.ProjectStoragePaths.encodeForFs; @@ -34,6 +35,7 @@ public class ConnectionStorage { private final Map sonarProjectStorageByKey = new ConcurrentHashMap<>(); private final Path projectsStorageRoot; private final PluginsStorage pluginsStorage; + private final WebApiErrorNotificationsStorage webApiErrorNotificationsStorage; private final Path connectionStorageRoot; public ConnectionStorage(Path globalStorageRoot, Path workDir, String connectionId) { @@ -41,6 +43,7 @@ public ConnectionStorage(Path globalStorageRoot, Path workDir, String connection this.projectsStorageRoot = connectionStorageRoot.resolve("projects"); this.serverIssueStoresManager = new ServerIssueStoresManager(projectsStorageRoot, workDir); this.serverInfoStorage = new ServerInfoStorage(connectionStorageRoot); + this.webApiErrorNotificationsStorage = new WebApiErrorNotificationsStorage(connectionStorageRoot); this.pluginsStorage = new PluginsStorage(connectionStorageRoot); } @@ -53,6 +56,10 @@ public SonarProjectStorage project(String sonarProjectKey) { k -> new SonarProjectStorage(projectsStorageRoot, serverIssueStoresManager, sonarProjectKey)); } + public WebApiErrorNotificationsStorage webApiErrorNotifications() { + return webApiErrorNotificationsStorage; + } + public PluginsStorage plugins() { return pluginsStorage; } diff --git a/backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/LocalStorageSynchronizer.java b/backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/LocalStorageSynchronizer.java index 6ac3084392..4dbcf66b62 100644 --- a/backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/LocalStorageSynchronizer.java +++ b/backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/LocalStorageSynchronizer.java @@ -89,6 +89,7 @@ public AnalyzerSettingsUpdateSummary synchronizeAnalyzerConfig(ServerApi serverA private AnalyzerConfiguration downloadAnalyzerConfig(ServerApi serverApi, String projectKey, SonarLintCancelMonitor cancelMonitor) { LOG.info("[SYNC] Synchronizing analyzer configuration for project '{}'", projectKey); + LOG.info("[SYNC] Languages enabled for synchronization: {}", enabledLanguageKeys); Map currentRuleSets; int currentSchemaVersion; try { diff --git a/backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ServerConnection.java b/backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ServerConnection.java deleted file mode 100644 index 2e034b89fe..0000000000 --- a/backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ServerConnection.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SonarLint Core - Server Connection - * Copyright (C) 2016-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonarsource.sonarlint.core.serverconnection; - -import java.nio.file.Path; -import java.util.LinkedHashSet; -import java.util.Set; -import java.util.stream.Collectors; -import org.sonarsource.sonarlint.core.commons.Version; -import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; -import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; -import org.sonarsource.sonarlint.core.serverapi.ServerApi; - -public class ServerConnection { - private static final Version CLEAN_CODE_TAXONOMY_MIN_SQ_VERSION = Version.create("10.2"); - - private final Set enabledLanguagesToSync; - private final LocalStorageSynchronizer storageSynchronizer; - private final boolean isSonarCloud; - private final ServerInfoSynchronizer serverInfoSynchronizer; - private final ConnectionStorage storage; - - public ServerConnection(Path globalStorageRoot, String connectionId, boolean isSonarCloud, Set enabledLanguages, Set embeddedPluginKeys, Path workDir) { - this(StorageFacadeCache.get().getOrCreate(globalStorageRoot, workDir), connectionId, isSonarCloud, enabledLanguages, embeddedPluginKeys); - } - - public ServerConnection(StorageFacade storageFacade, String connectionId, boolean isSonarCloud, Set enabledLanguages, Set embeddedPluginKeys) { - this.isSonarCloud = isSonarCloud; - this.enabledLanguagesToSync = enabledLanguages.stream().filter(SonarLanguage::shouldSyncInConnectedMode).collect(Collectors.toCollection(LinkedHashSet::new)); - - this.storage = storageFacade.connection(connectionId); - serverInfoSynchronizer = new ServerInfoSynchronizer(storage); - this.storageSynchronizer = new LocalStorageSynchronizer(enabledLanguagesToSync, embeddedPluginKeys, serverInfoSynchronizer, storage); - } - - public PluginSynchronizationSummary sync(ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { - return storageSynchronizer.synchronizeServerInfosAndPlugins(serverApi, cancelMonitor); - } - - public AnalyzerSettingsUpdateSummary sync(ServerApi serverApi, String projectKey, SonarLintCancelMonitor cancelMonitor) { - return storageSynchronizer.synchronizeAnalyzerConfig(serverApi, projectKey, cancelMonitor); - } - - public Version readOrSynchronizeServerVersion(ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { - return serverInfoSynchronizer.readOrSynchronizeServerInfo(serverApi, cancelMonitor).getVersion(); - } - - public Set getEnabledLanguagesToSync() { - return enabledLanguagesToSync; - } -} diff --git a/backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/WebApiErrorNotificationsStorage.java b/backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/WebApiErrorNotificationsStorage.java new file mode 100644 index 0000000000..34a2e439ec --- /dev/null +++ b/backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/WebApiErrorNotificationsStorage.java @@ -0,0 +1,62 @@ +/* + * SonarLint Core - Server Connection + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarsource.sonarlint.core.serverconnection.storage; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; +import org.sonarsource.sonarlint.core.serverconnection.FileUtils; +import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint; + +import static org.sonarsource.sonarlint.core.serverconnection.storage.ProtobufFileUtil.writeToFile; + +public class WebApiErrorNotificationsStorage { + + private static final SonarLintLogger LOG = SonarLintLogger.get(); + public static final String WEB_API_ERROR_NOTIFICATIONS = "web_api_error_notifications.pb"; + private final Path storageFilePath; + private final RWLock rwLock = new RWLock(); + + public WebApiErrorNotificationsStorage(Path projectStorageRoot) { + this.storageFilePath = projectStorageRoot.resolve(WEB_API_ERROR_NOTIFICATIONS); + } + + public void store(Long lastWebApiErrorNotification) { + FileUtils.mkdirs(storageFilePath.getParent()); + var valueToStore = adapt(lastWebApiErrorNotification); + LOG.debug("Storing last web API error notification in {}", storageFilePath); + rwLock.write(() -> writeToFile(valueToStore, storageFilePath)); + } + + public Optional readLastWebApiErrorNotification() { + return rwLock.read(() -> Files.exists(storageFilePath) ? + Optional.of(adapt(ProtobufFileUtil.readFile(storageFilePath, Sonarlint.LastWebApiErrorNotification.parser()))) : Optional.empty()); + } + + private static Sonarlint.LastWebApiErrorNotification adapt(Long lastWebApiErrorNotification) { + return Sonarlint.LastWebApiErrorNotification.newBuilder().setLastWrongTokenNotification(lastWebApiErrorNotification).build(); + } + + private static Long adapt(Sonarlint.LastWebApiErrorNotification lastWebApiErrorNotification) { + return lastWebApiErrorNotification.getLastWrongTokenNotification(); + } + +} diff --git a/backend/server-connection/src/main/proto/sonarlint.proto b/backend/server-connection/src/main/proto/sonarlint.proto index 3b88dd5f65..a423951e2d 100644 --- a/backend/server-connection/src/main/proto/sonarlint.proto +++ b/backend/server-connection/src/main/proto/sonarlint.proto @@ -100,6 +100,10 @@ message LastEventPolling { int64 last_event_polling = 1; } +message LastWebApiErrorNotification { + int64 last_wrong_token_notification = 1; +} + enum NewCodeDefinitionMode { UNKNOWN = 0; NUMBER_OF_DAYS = 1; diff --git a/client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/SonarLintRpcClientDelegate.java b/client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/SonarLintRpcClientDelegate.java index 388da30ccd..8999863be6 100644 --- a/client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/SonarLintRpcClientDelegate.java +++ b/client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/SonarLintRpcClientDelegate.java @@ -219,4 +219,7 @@ default Map getInferredAnalysisProperties(String configurationSc default Set getFileExclusions(String configurationScopeId) throws ConfigScopeNotFoundException { return Collections.emptySet(); } + + default void invalidToken(String connectionId) { + } } diff --git a/client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/SonarLintRpcClientImpl.java b/client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/SonarLintRpcClientImpl.java index ef7bcdd9f5..071a6e9ace 100644 --- a/client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/SonarLintRpcClientImpl.java +++ b/client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/SonarLintRpcClientImpl.java @@ -84,6 +84,7 @@ import org.sonarsource.sonarlint.core.rpc.protocol.client.promotion.PromoteExtraEnabledLanguagesInConnectedModeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.smartnotification.ShowSmartNotificationParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.sync.DidSynchronizeConfigurationScopeParams; +import org.sonarsource.sonarlint.core.rpc.protocol.client.sync.InvalidTokenParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.taint.vulnerability.DidChangeTaintVulnerabilitiesParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.TelemetryClientLiveAttributesResponse; @@ -423,4 +424,9 @@ public CompletableFuture getFileExclusions(GetFileExc } }); } + + @Override + public void invalidToken(InvalidTokenParams params) { + notify(() -> delegate.invalidToken(params.getConnectionId())); + } } diff --git a/medium-tests/src/test/java/mediumtest/EffectiveRulesMediumTests.java b/medium-tests/src/test/java/mediumtest/EffectiveRulesMediumTests.java index 0c268a4115..b766e462b7 100644 --- a/medium-tests/src/test/java/mediumtest/EffectiveRulesMediumTests.java +++ b/medium-tests/src/test/java/mediumtest/EffectiveRulesMediumTests.java @@ -246,7 +246,7 @@ void it_should_fail_to_merge_rule_from_storage_and_server_when_connection_is_unk assertThat(futureResponse).failsWithin(1, TimeUnit.SECONDS) .withThrowableOfType(ExecutionException.class) .withCauseInstanceOf(ResponseErrorException.class) - .withMessageContaining("Connection with ID 'connectionId' does not exist"); + .withMessageContaining("Connection 'connectionId' is gone"); } @Test diff --git a/medium-tests/src/test/java/mediumtest/NotebookLanguageMediumTests.java b/medium-tests/src/test/java/mediumtest/NotebookLanguageMediumTests.java index 10fab250ca..3140998678 100644 --- a/medium-tests/src/test/java/mediumtest/NotebookLanguageMediumTests.java +++ b/medium-tests/src/test/java/mediumtest/NotebookLanguageMediumTests.java @@ -19,8 +19,8 @@ */ package mediumtest; -import java.util.Set; import java.util.concurrent.ExecutionException; +import mediumtest.fixtures.SonarLintBackendFixture; import mediumtest.fixtures.SonarLintTestRpcServer; import mediumtest.fixtures.TestPlugin; import org.apache.commons.lang3.StringUtils; @@ -28,13 +28,15 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; -import org.sonarsource.sonarlint.core.serverconnection.ServerConnection; +import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.DidChangeCredentialsParams; +import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; +import static mediumtest.fixtures.ServerFixture.newSonarQubeServer; import static mediumtest.fixtures.SonarLintBackendFixture.newBackend; import static mediumtest.fixtures.SonarLintBackendFixture.newFakeClient; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; class NotebookLanguageMediumTests { @RegisterExtension @@ -42,17 +44,30 @@ class NotebookLanguageMediumTests { private static final String CONNECTION_ID = StringUtils.repeat("very-long-id", 30); private static final String JAVA_MODULE_KEY = "test-project-2"; + public static final String SCOPE_ID = "scopeId"; private static SonarLintTestRpcServer backend; + private static SonarLintBackendFixture.FakeSonarLintRpcClient client; @BeforeAll static void prepare() { - var fakeClient = newFakeClient() + client = newFakeClient() .build(); + + var server = newSonarQubeServer() + .withProject(JAVA_MODULE_KEY, project -> project.withBranch("main")) + .start(); backend = newBackend() - .withStorage(CONNECTION_ID, s -> s.withPlugins(TestPlugin.JAVASCRIPT, TestPlugin.JAVA) - .withProject("test-project") + .withSonarQubeConnection(CONNECTION_ID, server, storage -> storage + .withPlugin(TestPlugin.JAVA) + .withPlugin(TestPlugin.JAVASCRIPT) + .withPlugin(TestPlugin.PYTHON) .withProject(JAVA_MODULE_KEY)) - .build(fakeClient); + .withBoundConfigScope(SCOPE_ID, CONNECTION_ID, JAVA_MODULE_KEY) + .withEnabledLanguageInStandaloneMode(Language.JAVA) + .withEnabledLanguageInStandaloneMode(Language.JS) + .withEnabledLanguageInStandaloneMode(Language.IPYTHON) + .withFullSynchronization() + .build(client); } @AfterAll @@ -64,9 +79,9 @@ static void stop() throws ExecutionException, InterruptedException { @Test void should_not_enable_sync_for_notebook_python_language() { - var serverConnection = new ServerConnection(backend.getStorageRoot(), CONNECTION_ID, false, Set.of(SonarLanguage.JAVA, SonarLanguage.JS, - SonarLanguage.IPYTHON), Set.of(), backend.getWorkDir()); - assertThat(serverConnection.getEnabledLanguagesToSync()).containsOnly(SonarLanguage.JAVA, SonarLanguage.JS); + backend.getConnectionService().didChangeCredentials(new DidChangeCredentialsParams(CONNECTION_ID)); + + await().untilAsserted(() -> assertThat(client.getLogMessages()).contains("[SYNC] Languages enabled for synchronization: [java, js]")); } } diff --git a/medium-tests/src/test/java/mediumtest/fixtures/ServerFixture.java b/medium-tests/src/test/java/mediumtest/fixtures/ServerFixture.java index 6fa31f83a5..3eaa9f9b32 100644 --- a/medium-tests/src/test/java/mediumtest/fixtures/ServerFixture.java +++ b/medium-tests/src/test/java/mediumtest/fixtures/ServerFixture.java @@ -36,6 +36,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.UnaryOperator; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -113,6 +114,7 @@ public static class ServerBuilder { private ServerStatus serverStatus = ServerStatus.UP; private boolean smartNotificationsSupported; private final List tokensRegistered = new ArrayList<>(); + private Integer statusCode = 200; public ServerBuilder(ServerKind serverKind, @Nullable String organizationKey, @Nullable String version) { this.serverKind = serverKind; @@ -166,8 +168,14 @@ public ServerBuilder withVersion(String version) { return this; } + public ServerBuilder withResponseCode(Integer status) { + this.statusCode = status; + return this; + } + public Server start() { - var server = new Server(serverKind, serverStatus, organizationKey, version, projectByProjectKey, smartNotificationsSupported, pluginsByKey, qualityProfilesByKey, tokensRegistered); + var server = new Server(serverKind, serverStatus, organizationKey, version, projectByProjectKey, smartNotificationsSupported, pluginsByKey, + qualityProfilesByKey, tokensRegistered, statusCode); server.start(); return server; } @@ -515,11 +523,12 @@ public static class Server { private final Map pluginsByKey; private final Map qualityProfilesByKey; private final List tokensRegistered; + private final Integer statusCode; public Server(ServerKind serverKind, ServerStatus serverStatus, @Nullable String organizationKey, @Nullable String version, Map projectsByProjectKey, boolean smartNotificationsSupported, Map pluginsByKey, - Map qualityProfilesByKey, List tokensRegistered) { + Map qualityProfilesByKey, List tokensRegistered, Integer statusCode) { this.serverKind = serverKind; this.serverStatus = serverStatus; this.organizationKey = organizationKey; @@ -529,6 +538,7 @@ public Server(ServerKind serverKind, ServerStatus serverStatus, @Nullable String this.pluginsByKey = pluginsByKey; this.qualityProfilesByKey = qualityProfilesByKey; this.tokensRegistered = tokensRegistered; + this.statusCode = statusCode; } public void start() { @@ -567,8 +577,11 @@ private void registerComponentApiResponses() { } public void registerSystemApiResponses() { + // API is public, so it can't return 401 or 403 status + var statusesToSkip = Set.of(401, 403); + var status = statusesToSkip.contains(statusCode) ? 200 : statusCode; mockServer.stubFor(get("/api/system/status") - .willReturn(aResponse().withStatus(200).withBody("{\"id\": \"20160308094653\",\"version\": \"" + version + "\",\"status\": " + + .willReturn(aResponse().withStatus(status).withBody("{\"id\": \"20160308094653\",\"version\": \"" + version + "\",\"status\": " + "\"" + serverStatus + "\"}"))); } @@ -579,7 +592,7 @@ private void registerPluginsApiResponses() { private void registerPluginsInstalledResponses() { mockServer.stubFor(get("/api/plugins/installed") - .willReturn(aResponse().withStatus(200).withBody("{\"plugins\": [" + + .willReturn(aResponse().withStatus(statusCode).withBody("{\"plugins\": [" + pluginsByKey.entrySet().stream().map( entry -> { var pluginKey = entry.getKey(); @@ -597,7 +610,7 @@ private void registerPluginsDownloadResponses() { try { var pluginContent = Files.exists(plugin.jarPath) ? Files.readAllBytes(plugin.jarPath) : new byte[0]; mockServer.stubFor(get("/api/plugins/download?plugin=" + pluginKey) - .willReturn(aResponse().withStatus(200).withBody(pluginContent))); + .willReturn(aResponse().withStatus(statusCode).withBody(pluginContent))); } catch (IOException e) { throw new RuntimeException(e); } @@ -611,7 +624,7 @@ private void registerQualityProfilesApiResponses() { urlBuilder.append("&organization=").append(organizationKey); } mockServer.stubFor(get(urlBuilder.toString()) - .willReturn(aResponse().withStatus(200).withResponseBody(protobufBody(Qualityprofiles.SearchWsResponse.newBuilder().addAllProfiles( + .willReturn(aResponse().withStatus(statusCode).withResponseBody(protobufBody(Qualityprofiles.SearchWsResponse.newBuilder().addAllProfiles( project.qualityProfileKeys.stream().map(qualityProfileKey -> { var qualityProfile = qualityProfilesByKey.get(qualityProfileKey); return Qualityprofiles.SearchWsResponse.QualityProfile.newBuilder() @@ -636,7 +649,7 @@ private void registerRulesApiResponses() { } url += "&activation=true&f=templateKey,actives&types=CODE_SMELL,BUG,VULNERABILITY,SECURITY_HOTSPOT&s=key&ps=500&p=1"; mockServer.stubFor(get(url) - .willReturn(aResponse().withStatus(200).withResponseBody(protobufBody(Rules.SearchResponse.newBuilder() + .willReturn(aResponse().withStatus(statusCode).withResponseBody(protobufBody(Rules.SearchResponse.newBuilder() .addAllRules(qualityProfile.activeRulesByKey.entrySet().stream().map(entry -> Rules.Rule.newBuilder() .setKey(entry.getKey()) .setSeverity(entry.getValue().issueSeverity.name()) @@ -656,7 +669,7 @@ private void registerRulesApiResponses() { } url += "&f=repo&s=key&ps=500&p=1"; mockServer.stubFor(get(url) - .willReturn(aResponse().withStatus(200).withResponseBody(protobufBody(Rules.SearchResponse.newBuilder() + .willReturn(aResponse().withStatus(statusCode).withResponseBody(protobufBody(Rules.SearchResponse.newBuilder() .addAllRules(taintActiveRulesByKey.entrySet().stream().map(entry -> Rules.Rule.newBuilder() .setKey(entry.getKey()) .setSeverity(entry.getValue().issueSeverity.name()) @@ -674,7 +687,7 @@ private void registerRulesApiResponses() { var rule = entry.getValue(); var rulesShowUrl = "/api/rules/show.protobuf?key=" + ruleKey; mockServer.stubFor(get(rulesShowUrl) - .willReturn(aResponse().withStatus(200).withResponseBody(protobufBody(Rules.ShowResponse.newBuilder() + .willReturn(aResponse().withStatus(statusCode).withResponseBody(protobufBody(Rules.ShowResponse.newBuilder() .setRule(Rules.Rule.newBuilder() .setKey(ruleKey) .setName("fakeName") @@ -694,7 +707,7 @@ private void registerRulesApiResponses() { private void registerProjectBranchesApiResponses() { projectsByProjectKey.forEach((projectKey, project) -> mockServer.stubFor(get("/api/project_branches/list.protobuf?project=" + projectKey) - .willReturn(aResponse().withStatus(200).withResponseBody(protobufBody(ProjectBranches.ListWsResponse.newBuilder() + .willReturn(aResponse().withStatus(statusCode).withResponseBody(protobufBody(ProjectBranches.ListWsResponse.newBuilder() .addAllBranches(project.branchesByName.keySet().stream() .filter(Objects::nonNull) .map(branchName -> ProjectBranches.Branch.newBuilder().setName(branchName).setIsMain(project.mainBranchName.equals(branchName)).setType(Common.BranchType.LONG).build()) @@ -911,7 +924,7 @@ private void registerHotspotsShowApiResponses() { } private void registerHotspotsStatusChangeApiResponses() { - mockServer.stubFor(post("/api/hotspots/change_status").willReturn(aResponse().withStatus(200))); + mockServer.stubFor(post("/api/hotspots/change_status").willReturn(aResponse().withStatus(statusCode))); } private void registerIssuesApiResponses() { @@ -962,11 +975,11 @@ private void registerBatchIssuesResponses() { } private void registerIssuesStatusChangeApiResponses() { - mockServer.stubFor(post("/api/issues/do_transition").willReturn(aResponse().withStatus(200))); + mockServer.stubFor(post("/api/issues/do_transition").willReturn(aResponse().withStatus(statusCode))); } private void registerAddIssueCommentApiResponses() { - mockServer.stubFor(post("/api/issues/add_comment").willReturn(aResponse().withStatus(200))); + mockServer.stubFor(post("/api/issues/add_comment").willReturn(aResponse().withStatus(statusCode))); } private void registerApiIssuesPullResponses() { @@ -1031,7 +1044,7 @@ private void registerApiIssuesPullTaintResponses() { } private void registerIssueAnticipateTransitionResponses() { - mockServer.stubFor(post("/api/issues/anticipated_transitions?projectKey=projectKey").willReturn(aResponse().withStatus(200))); + mockServer.stubFor(post("/api/issues/anticipated_transitions?projectKey=projectKey").willReturn(aResponse().withStatus(statusCode))); } private void registerSourceApiResponses() { @@ -1053,7 +1066,7 @@ private void registerSourceApiResponses() { private void registerDevelopersApiResponses() { if (smartNotificationsSupported) { - mockServer.stubFor(get("/api/developers/search_events?projects=&from=").willReturn(aResponse().withStatus(200))); + mockServer.stubFor(get("/api/developers/search_events?projects=&from=").willReturn(aResponse().withStatus(statusCode))); } } @@ -1142,7 +1155,7 @@ private void registerSettingsApiResponses() { } private void registerTokenApiResponse() { - tokensRegistered.forEach(tokenName -> mockServer.stubFor(post("/api/user_tokens/revoke").withRequestBody(WireMock.containing("name=" + tokenName)).willReturn(aResponse().withStatus(200)))); + tokensRegistered.forEach(tokenName -> mockServer.stubFor(post("/api/user_tokens/revoke").withRequestBody(WireMock.containing("name=" + tokenName)).willReturn(aResponse().withStatus(statusCode)))); } public void shutdown() { diff --git a/medium-tests/src/test/java/mediumtest/fixtures/SonarLintBackendFixture.java b/medium-tests/src/test/java/mediumtest/fixtures/SonarLintBackendFixture.java index 5cefad898d..af1a6f55a6 100644 --- a/medium-tests/src/test/java/mediumtest/fixtures/SonarLintBackendFixture.java +++ b/medium-tests/src/test/java/mediumtest/fixtures/SonarLintBackendFixture.java @@ -621,6 +621,7 @@ public static class FakeSonarLintRpcClient implements SonarLintRpcClientDelegate Map>> raisedHotspotsByScopeId = new HashMap<>(); Map> inferredAnalysisPropertiesByScopeId = new HashMap<>(); Map analysisReadinessPerScopeId = new HashMap<>(); + Set connectionIdsWithInvalidToken = new HashSet<>(); public FakeSonarLintRpcClient(Map> credentialsByConnectionId, boolean printLogsToStdOut, Map matchedBranchPerScopeId, Map baseDirsByConfigScope, Map> initialFilesByConfigScope, Map> fileExclusionsByConfigScope) { @@ -689,6 +690,11 @@ public void reportProgress(ReportProgressParams params) { } } + @Override + public void invalidToken(String connectionId) { + connectionIdsWithInvalidToken.add(connectionId); + } + public Map getProgressReportsByTaskId() { return progressReportsByTaskId; } @@ -891,6 +897,10 @@ public void waitForSynchronization() { verify(this, timeout(5000)).didSynchronizeConfigurationScopes(any()); } + public Set getConnectionIdsWithInvalidToken() { + return connectionIdsWithInvalidToken; + } + public static class ProgressReport { @CheckForNull private final String configurationScopeId; diff --git a/medium-tests/src/test/java/mediumtest/synchronization/ConnectionSyncMediumTests.java b/medium-tests/src/test/java/mediumtest/synchronization/ConnectionSyncMediumTests.java index fd5827089a..d8e6a8cbaf 100644 --- a/medium-tests/src/test/java/mediumtest/synchronization/ConnectionSyncMediumTests.java +++ b/medium-tests/src/test/java/mediumtest/synchronization/ConnectionSyncMediumTests.java @@ -26,6 +26,8 @@ import mediumtest.fixtures.TestPlugin; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.api.TextRange; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer; @@ -119,7 +121,7 @@ void it_should_sync_when_credentials_are_updated() { RuleType.VULNERABILITY))) .start(); - server.getMockServer().stubFor(get("/api/system/status").willReturn(aResponse().withStatus(401))); + server.getMockServer().stubFor(get("/api/system/status").willReturn(aResponse().withStatus(404))); backend = newBackend() .withSonarQubeConnection(CONNECTION_ID, server, storage -> storage.withPlugin(TestPlugin.JAVA)) @@ -139,6 +141,33 @@ void it_should_sync_when_credentials_are_updated() { "Synchronizing project branches for project 'projectKey'")); } + @ParameterizedTest + @ValueSource(ints = {401, 403}) + void it_should_notify_client_if_invalid_token(Integer status) { + var client = newFakeClient() + .withCredentials(CONNECTION_ID, "user", "pw") + .build(); + when(client.getClientLiveDescription()).thenReturn(this.getClass().getName()); + + var server = newSonarQubeServer() + .withProject("projectKey", project -> project.withBranch("main")) + .withResponseCode(status) + .start(); + + backend = newBackend() + .withSonarQubeConnection(CONNECTION_ID, server, storage -> storage.withPlugin(TestPlugin.JAVA).withProject("projectKey")) + .withBoundConfigScope(SCOPE_ID, CONNECTION_ID, "projectKey") + .withEnabledLanguageInStandaloneMode(JAVA) + .withProjectSynchronization() + .withFullSynchronization() + .build(client); + await().untilAsserted(() -> assertThat(client.getLogMessages()).contains("Error during synchronization")); + + backend.getConnectionService().didChangeCredentials(new DidChangeCredentialsParams(CONNECTION_ID)); + + await().untilAsserted(() -> assertThat(client.getConnectionIdsWithInvalidToken()).containsExactly(CONNECTION_ID)); + } + private EffectiveRuleDetailsDto getEffectiveRuleDetails(String configScopeId, String ruleKey) { return getEffectiveRuleDetails(configScopeId, ruleKey, null); } diff --git a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/SonarLintRpcClient.java b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/SonarLintRpcClient.java index 82f7bbbdc9..38487d9a3c 100644 --- a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/SonarLintRpcClient.java +++ b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/SonarLintRpcClient.java @@ -73,6 +73,7 @@ import org.sonarsource.sonarlint.core.rpc.protocol.client.promotion.PromoteExtraEnabledLanguagesInConnectedModeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.smartnotification.ShowSmartNotificationParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.sync.DidSynchronizeConfigurationScopeParams; +import org.sonarsource.sonarlint.core.rpc.protocol.client.sync.InvalidTokenParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.taint.vulnerability.DidChangeTaintVulnerabilitiesParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.TelemetryClientLiveAttributesResponse; @@ -346,4 +347,8 @@ default void promoteExtraEnabledLanguagesInConnectedMode(PromoteExtraEnabledLang */ @JsonRequest CompletableFuture getFileExclusions(GetFileExclusionsParams params); + + @JsonNotification + default void invalidToken(InvalidTokenParams params) { + } } diff --git a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/client/sync/InvalidTokenParams.java b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/client/sync/InvalidTokenParams.java new file mode 100644 index 0000000000..c376e04b40 --- /dev/null +++ b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/client/sync/InvalidTokenParams.java @@ -0,0 +1,33 @@ +/* + * SonarLint Core - RPC Protocol + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarsource.sonarlint.core.rpc.protocol.client.sync; + +public class InvalidTokenParams { + + String connectionId; + + public InvalidTokenParams(String connectionId) { + this.connectionId = connectionId; + } + + public String getConnectionId() { + return connectionId; + } +}