Skip to content

Commit

Permalink
SLCORE-671 Suggest automatic binding following connection creation (#838
Browse files Browse the repository at this point in the history
)
  • Loading branch information
nquinquenel committed Jan 29, 2024
1 parent c90d51e commit 2e6e3db
Show file tree
Hide file tree
Showing 28 changed files with 528 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.sonarsource.sonarlint.core.clientapi.client.OpenUrlInBrowserParams;
import org.sonarsource.sonarlint.core.clientapi.client.binding.AssistBindingParams;
import org.sonarsource.sonarlint.core.clientapi.client.binding.AssistBindingResponse;
import org.sonarsource.sonarlint.core.clientapi.client.binding.NoBindingSuggestionFoundParams;
import org.sonarsource.sonarlint.core.clientapi.client.binding.SuggestBindingParams;
import org.sonarsource.sonarlint.core.clientapi.client.connection.AssistCreatingConnectionParams;
import org.sonarsource.sonarlint.core.clientapi.client.connection.AssistCreatingConnectionResponse;
Expand Down Expand Up @@ -180,4 +181,8 @@ default CompletableFuture<CheckServerTrustedResponse> checkServerTrusted(CheckSe
default void didReceiveServerEvent(DidReceiveServerEventParams params) {
// not implemented
}

@JsonNotification
void noBindingSuggestionFound(NoBindingSuggestionFoundParams params);

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;

public interface UserTokenService {

/**
* <p> It revokes a user token that is existing on the server and was handed over to the client.
* <p> It revokes a user token that is existing on the server.
* It silently deals with the following conditions:
* <ul>
* <li>the token provided by name (identified by {@link RevokeTokenParams#getTokenName()} exists</li>
Expand All @@ -38,6 +39,6 @@ public interface UserTokenService {
* </ul>
* </p>
*/
@JsonRequest
CompletableFuture<Void> revokeToken(RevokeTokenParams params);

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
public class AssistBindingParams {
private final String connectionId;
private final String projectKey;
private final String configScopeId;

public AssistBindingParams(String connectionId, String projectKey) {
public AssistBindingParams(String connectionId, String projectKey, String configScopeId) {
this.connectionId = connectionId;
this.projectKey = projectKey;
this.configScopeId = configScopeId;
}

public String getConnectionId() {
Expand All @@ -35,4 +37,8 @@ public String getConnectionId() {
public String getProjectKey() {
return projectKey;
}

public String getConfigScopeId() {
return configScopeId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@
*/
package org.sonarsource.sonarlint.core.clientapi.client.binding;

import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
import javax.annotation.Nullable;

public class AssistBindingResponse {
private final String configurationScopeId;

public AssistBindingResponse(@NonNull String configurationScopeId) {
public AssistBindingResponse(@Nullable String configurationScopeId) {
this.configurationScopeId = configurationScopeId;
}

@NonNull
@Nullable
public String getConfigurationScopeId() {
return configurationScopeId;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* SonarLint Core - Client 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.clientapi.client.binding;

import org.eclipse.lsp4j.jsonrpc.validation.NonNull;

public class NoBindingSuggestionFoundParams {

@NonNull
private final String projectKey;

public NoBindingSuggestionFoundParams(String projectKey) {
this.projectKey = projectKey;
}

public String getProjectKey() {
return projectKey;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,24 @@
*/
package org.sonarsource.sonarlint.core.clientapi.client.connection;

import java.util.Set;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;

public class AssistCreatingConnectionResponse {
private final String newConnectionId;
private final Set<String> configScopeIds;

public AssistCreatingConnectionResponse(@NonNull String newConnectionId) {
public AssistCreatingConnectionResponse(@NonNull String newConnectionId, @NonNull Set<String> configScopeIds) {
this.newConnectionId = newConnectionId;
this.configScopeIds = configScopeIds;
}

@NonNull
public String getNewConnectionId() {
return newConnectionId;
}
@NonNull
public Set<String> getConfigScopeIds() {
return configScopeIds;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.sonarsource.sonarlint.core.clientapi.client.OpenUrlInBrowserParams;
import org.sonarsource.sonarlint.core.clientapi.client.binding.AssistBindingParams;
import org.sonarsource.sonarlint.core.clientapi.client.binding.AssistBindingResponse;
import org.sonarsource.sonarlint.core.clientapi.client.binding.NoBindingSuggestionFoundParams;
import org.sonarsource.sonarlint.core.clientapi.client.binding.SuggestBindingParams;
import org.sonarsource.sonarlint.core.clientapi.client.connection.AssistCreatingConnectionParams;
import org.sonarsource.sonarlint.core.clientapi.client.connection.AssistCreatingConnectionResponse;
Expand Down Expand Up @@ -135,7 +136,7 @@ protected PasswordAuthentication getPasswordAuthentication() {
}

@Test
void failIfInvalidURL() throws ExecutionException, InterruptedException {
void failIfInvalidURL() {
var future = underTest.getProxyPasswordAuthentication(new GetProxyPasswordAuthenticationParams("http://foo", 8085, "protocol", "prompt", "scheme", "invalid:url"));
assertThat(future).failsWithin(Duration.ofMillis(50))
.withThrowableOfType(ExecutionException.class)
Expand Down Expand Up @@ -217,5 +218,11 @@ public void didSynchronizeConfigurationScopes(DidSynchronizeConfigurationScopePa
public CompletableFuture<GetCredentialsResponse> getCredentials(GetCredentialsParams params) {
return null;
}

@Override
public void noBindingSuggestionFound(NoBindingSuggestionFoundParams params) {

}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,17 @@ public BindingClueProvider(ConnectionConfigurationRepository connectionRepositor
this.client = client;
}

public List<BindingClueWithConnections> collectBindingCluesWithConnections(String configScopeId, Set<String> connectionIds) throws InterruptedException {
public List<BindingClueWithConnections> collectBindingCluesWithConnections(String configScopeId, Set<String> connectionIds, @Nullable String projectKey)
throws InterruptedException {
var bindingClues = collectBindingClues(configScopeId);
if (projectKey != null && connectionIds.size() == 1) {
for (var bindingClue : bindingClues) {
var sonarProjectKey = bindingClue.getSonarProjectKey();
if (sonarProjectKey != null && sonarProjectKey.equals(projectKey)) {
return List.of(new BindingClueWithConnections(bindingClue, connectionIds));
}
}
}
return matchConnections(bindingClues, connectionIds);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Named;
Expand Down Expand Up @@ -138,7 +139,7 @@ public void connectionAdded(ConnectionConfigurationAddedEvent event) {
@Override
public CompletableFuture<GetBindingSuggestionsResponse> getBindingSuggestions(GetBindingSuggestionParams params) {
return CompletableFuture.supplyAsync(() -> {
var suggestions = computeBindingSuggestions(Set.of(params.getConfigScopeId()), Set.of(params.getConnectionId()));
var suggestions = computeBindingSuggestions(Set.of(params.getConfigScopeId()), Set.of(params.getConnectionId()), null);
return new GetBindingSuggestionsResponse(suggestions);
}, executorService);
}
Expand All @@ -154,14 +155,14 @@ private void queueBindingSuggestionComputation(Set<String> configScopeIds, Set<S
}

private void computeAndNotifyBindingSuggestions(Set<String> configScopeIds, Set<String> candidateConnectionIds) {
Map<String, List<BindingSuggestionDto>> suggestions = computeBindingSuggestions(configScopeIds, candidateConnectionIds);
Map<String, List<BindingSuggestionDto>> suggestions = computeBindingSuggestions(configScopeIds, candidateConnectionIds, null);
if (!suggestions.isEmpty()) {
client.suggestBinding(new SuggestBindingParams(suggestions));
}
}

@NonNull
private Map<String, List<BindingSuggestionDto>> computeBindingSuggestions(Set<String> configScopeIds, Set<String> candidateConnectionIds) {
public Map<String, List<BindingSuggestionDto>> computeBindingSuggestions(Set<String> configScopeIds, Set<String> candidateConnectionIds, @Nullable String projectKey) {
var eligibleConfigScopesForBindingSuggestion = new HashSet<String>();
for (var configScopeId : configScopeIds) {
if (isScopeEligibleForBindingSuggestion(configScopeId)) {
Expand All @@ -177,7 +178,7 @@ private Map<String, List<BindingSuggestionDto>> computeBindingSuggestions(Set<St

try {
for (var configScopeId : eligibleConfigScopesForBindingSuggestion) {
var scopeSuggestions = suggestBindingForEligibleScope(configScopeId, candidateConnectionIds);
var scopeSuggestions = suggestBindingForEligibleScope(configScopeId, candidateConnectionIds, projectKey);
LOG.debug("Found {} {} for configuration scope '{}'", scopeSuggestions.size(), singlePlural(scopeSuggestions.size(), "suggestion", "suggestions"), configScopeId);
suggestions.put(configScopeId, scopeSuggestions);
}
Expand All @@ -189,13 +190,17 @@ private Map<String, List<BindingSuggestionDto>> computeBindingSuggestions(Set<St
return suggestions;
}

private List<BindingSuggestionDto> suggestBindingForEligibleScope(String checkedConfigScopeId, Set<String> candidateConnectionIds) throws InterruptedException {
var cluesAndConnections = bindingClueProvider.collectBindingCluesWithConnections(checkedConfigScopeId, candidateConnectionIds);
private List<BindingSuggestionDto> suggestBindingForEligibleScope(String checkedConfigScopeId, Set<String> candidateConnectionIds, @Nullable String projectKey)
throws InterruptedException {
var cluesAndConnections = bindingClueProvider.collectBindingCluesWithConnections(checkedConfigScopeId, candidateConnectionIds, projectKey);

List<BindingSuggestionDto> suggestions = new ArrayList<>();
var cluesWithProjectKey = cluesAndConnections.stream().filter(c -> c.getBindingClue().getSonarProjectKey() != null).collect(toList());
for (var bindingClueWithConnections : cluesWithProjectKey) {
var sonarProjectKey = requireNonNull(bindingClueWithConnections.getBindingClue().getSonarProjectKey());
if (sonarProjectKey.equals(projectKey)) {
return suggestions;
}
for (var connectionId : bindingClueWithConnections.getConnectionIds()) {
sonarProjectsCache
.getSonarProject(connectionId, sonarProjectKey)
Expand All @@ -207,25 +212,25 @@ private List<BindingSuggestionDto> suggestBindingForEligibleScope(String checked
if (isNotBlank(configScopeName)) {
var cluesWithoutProjectKey = cluesAndConnections.stream().filter(c -> c.getBindingClue().getSonarProjectKey() == null).collect(toList());
for (var bindingClueWithConnections : cluesWithoutProjectKey) {
searchGoodMatchInConnections(suggestions, configScopeName, bindingClueWithConnections.getConnectionIds());
searchGoodMatchInConnections(suggestions, configScopeName, bindingClueWithConnections.getConnectionIds(), projectKey);
}
if (cluesWithoutProjectKey.isEmpty()) {
searchGoodMatchInConnections(suggestions, configScopeName, candidateConnectionIds);
searchGoodMatchInConnections(suggestions, configScopeName, candidateConnectionIds, projectKey);
}
}
}
return suggestions;
}

private void searchGoodMatchInConnections(List<BindingSuggestionDto> suggestions, String configScopeName, Set<String> connectionIdsToSearch) {
private void searchGoodMatchInConnections(List<BindingSuggestionDto> suggestions, String configScopeName, Set<String> connectionIdsToSearch, @Nullable String projectKey) {
for (var connectionId : connectionIdsToSearch) {
searchGoodMatchInConnection(suggestions, configScopeName, connectionId);
searchGoodMatchInConnection(suggestions, configScopeName, connectionId, projectKey);
}
}

private void searchGoodMatchInConnection(List<BindingSuggestionDto> suggestions, String configScopeName, String connectionId) {
private void searchGoodMatchInConnection(List<BindingSuggestionDto> suggestions, String configScopeName, String connectionId, @Nullable String projectKey) {
LOG.debug("Attempt to find a good match for '{}' on connection '{}'...", configScopeName, connectionId);
var index = sonarProjectsCache.getTextSearchIndex(connectionId);
var index = sonarProjectsCache.getTextSearchIndexCached(connectionId, projectKey);
var searchResult = index.search(configScopeName);
if (!searchResult.isEmpty()) {
Double bestScore = Double.MIN_VALUE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.inject.Named;
import javax.inject.Singleton;
import org.sonarsource.sonarlint.core.client.api.util.TextSearchIndex;
Expand Down Expand Up @@ -117,30 +118,41 @@ public Optional<ServerProject> getSonarProject(String connectionId, String sonar
}
}

public TextSearchIndex<ServerProject> getTextSearchIndex(String connectionId) {
public TextSearchIndex<ServerProject> getTextSearchIndexCached(String connectionId, @Nullable String projectKey) {
try {
return textSearchIndexCache.get(connectionId, () -> {
LOG.debug("Load projects from connection '{}'...", connectionId);
List<ServerProject> projects;
try {
projects = serverApiProvider.getServerApi(connectionId).map(s -> s.component().getAllProjects(new ProgressMonitor(null))).orElse(List.of());
} catch (Exception e) {
LOG.error("Error while querying projects from connection '{}'", connectionId, e);
return new TextSearchIndex<>();
}
if (projects.isEmpty()) {
LOG.debug("No projects found for connection '{}'", connectionId);
return new TextSearchIndex<>();
} else {
LOG.debug("Creating index for {} {}", projects.size(), singlePlural(projects.size(), "project", "projects"));
var index = new TextSearchIndex<ServerProject>();
projects.forEach(p -> index.index(p, p.getKey() + " " + p.getName()));
return index;
}
});
if (projectKey != null) {
return getTextSearchIndex(connectionId, projectKey);
}
return textSearchIndexCache.get(connectionId, () -> getTextSearchIndex(connectionId, null));
} catch (ExecutionException e) {
throw new IllegalStateException(e.getCause());
}
}

private TextSearchIndex<ServerProject> getTextSearchIndex(String connectionId, @Nullable String projectKey) {
LOG.debug("Load projects from connection '{}'...", connectionId);
List<ServerProject> projects;
try {
if (projectKey != null) {
var optProject = serverApiProvider.getServerApi(connectionId).map(s -> s.component().getProject(projectKey))
.orElseThrow(() -> new IllegalStateException("Project not found"));
projects = optProject.map(List::of).orElseGet(List::of);
} else {
projects = serverApiProvider.getServerApi(connectionId).map(s -> s.component().getAllProjects(new ProgressMonitor(null))).orElse(List.of());
}
} catch (Exception e) {
LOG.error("Error while querying projects from connection '{}'", connectionId, e);
return new TextSearchIndex<>();
}
if (projects.isEmpty()) {
LOG.debug("No projects found for connection '{}'", connectionId);
return new TextSearchIndex<>();
} else {
LOG.debug("Creating index for {} {}", projects.size(), singlePlural(projects.size(), "project", "projects"));
var index = new TextSearchIndex<ServerProject>();
projects.forEach(p -> index.index(p, p.getKey() + " " + p.getName()));
return index;
}
}

}
Loading

0 comments on commit 2e6e3db

Please sign in to comment.