import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.server.component.index.ComponentIndex;
import org.sonar.server.component.index.ComponentIndexQuery;
import org.sonar.server.component.index.ComponentsPerQualifier;
+import org.sonar.server.es.textsearch.ComponentTextSearchFeature;
import org.sonarqube.ws.WsComponents.Component;
import org.sonarqube.ws.WsComponents.SuggestionsWsResponse;
import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Qualifier;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Arrays.asList;
+import static java.util.Optional.ofNullable;
import static org.sonar.core.util.stream.MoreCollectors.toList;
+import static org.sonar.server.es.DefaultIndexSettings.MINIMUM_NGRAM_LENGTH;
import static org.sonar.server.ws.WsUtils.writeProtobuf;
import static org.sonarqube.ws.client.component.ComponentsWsParameters.ACTION_SUGGESTIONS;
static final String PARAM_QUERY = "s";
static final String PARAM_MORE = "more";
+ static final String SHORT_INPUT_WARNING = "short_input";
private static final String[] QUALIFIERS = {
Qualifiers.VIEW,
action.createParam(PARAM_QUERY)
.setRequired(true)
- .setDescription("Substring of project key (minimum 2 characters)")
+ .setDescription("Search query with a minimum of two characters. Can contain several search tokens, separated by spaces. " +
+ "Search tokens with only one character will be ignored.")
.setExampleValue("sonar");
action.createParam(PARAM_MORE)
String more = wsRequest.param(PARAM_MORE);
ComponentIndexQuery.Builder queryBuilder = ComponentIndexQuery.builder().setQuery(query);
+
+ List<ComponentsPerQualifier> componentsPerQualifiers = getComponentsPerQualifiers(more, queryBuilder);
+ String warning = getWarning(query);
+
+ SuggestionsWsResponse searchWsResponse = toResponse(componentsPerQualifiers, warning);
+ writeProtobuf(searchWsResponse, wsRequest, wsResponse);
+ }
+
+ private static String getWarning(String query) {
+ List<String> tokens = ComponentTextSearchFeature.split(query).collect(Collectors.toList());
+ if (tokens.stream().anyMatch(token -> token.length() < MINIMUM_NGRAM_LENGTH)) {
+ return SHORT_INPUT_WARNING;
+ }
+ return null;
+ }
+
+ private List<ComponentsPerQualifier> getComponentsPerQualifiers(String more, ComponentIndexQuery.Builder queryBuilder) {
+ List<ComponentsPerQualifier> componentsPerQualifiers;
if (more == null) {
queryBuilder.setQualifiers(asList(QUALIFIERS))
.setLimit(DEFAULT_LIMIT);
queryBuilder.setQualifiers(Collections.singletonList(more))
.setLimit(EXTENDED_LIMIT);
}
- List<ComponentsPerQualifier> componentsPerQualifiers = searchInIndex(queryBuilder.build());
- SuggestionsWsResponse searchWsResponse = toResponse(componentsPerQualifiers);
- writeProtobuf(searchWsResponse, wsRequest, wsResponse);
+ componentsPerQualifiers = searchInIndex(queryBuilder.build());
+ return componentsPerQualifiers;
}
private List<ComponentsPerQualifier> searchInIndex(ComponentIndexQuery componentIndexQuery) {
return index.search(componentIndexQuery);
}
- private SuggestionsWsResponse toResponse(List<ComponentsPerQualifier> componentsPerQualifiers) {
- return SuggestionsWsResponse.newBuilder()
- .addAllResults(getResultsOfAllQualifiers(componentsPerQualifiers))
- .build();
+ private SuggestionsWsResponse toResponse(List<ComponentsPerQualifier> componentsPerQualifiers, @Nullable String warning) {
+ SuggestionsWsResponse.Builder builder = SuggestionsWsResponse.newBuilder()
+ .addAllResults(getResultsOfAllQualifiers(componentsPerQualifiers));
+ ofNullable(warning).ifPresent(builder::setWarning);
+ return builder.build();
}
private List<Qualifier> getResultsOfAllQualifiers(List<ComponentsPerQualifier> componentsPerQualifiers) {
import static org.sonar.server.es.DefaultIndexSettings.LOWERCASE;
import static org.sonar.server.es.DefaultIndexSettings.MAXIMUM_NGRAM_LENGTH;
import static org.sonar.server.es.DefaultIndexSettings.MAX_GRAM;
+import static org.sonar.server.es.DefaultIndexSettings.MINIMUM_NGRAM_LENGTH;
import static org.sonar.server.es.DefaultIndexSettings.MIN_GRAM;
import static org.sonar.server.es.DefaultIndexSettings.PATTERN;
import static org.sonar.server.es.DefaultIndexSettings.PORTER_STEM;
set("stem_english_possessive", true);
}
},
- EDGE_NGRAM_FILTER(FILTER) {
-
- @Override
- protected void setup() {
- set(TYPE, "edge_ngram");
- set(MIN_GRAM, 1);
- set(MAX_GRAM, 15);
- }
- },
NGRAM_FILTER(FILTER) {
@Override
protected void setup() {
set(TYPE, "nGram");
- set(MIN_GRAM, 2);
- set(MAX_GRAM, 15);
+ set(MIN_GRAM, MINIMUM_NGRAM_LENGTH);
+ set(MAX_GRAM, MAXIMUM_NGRAM_LENGTH);
setArray("token_chars", "letter", "digit", "punctuation", "symbol");
}
},
@Override
protected void setup() {
set(TYPE, "nGram");
- set(MIN_GRAM, 2);
+ set(MIN_GRAM, MINIMUM_NGRAM_LENGTH);
set(MAX_GRAM, MAXIMUM_NGRAM_LENGTH);
setArray("token_chars", "letter", "digit", "punctuation", "symbol");
}
import static org.sonar.db.component.ComponentTesting.newProjectDto;
import static org.sonar.server.component.ws.SuggestionsAction.DEFAULT_LIMIT;
import static org.sonar.server.component.ws.SuggestionsAction.EXTENDED_LIMIT;
+import static org.sonar.server.component.ws.SuggestionsAction.SHORT_INPUT_WARNING;
import static org.sonar.server.component.ws.SuggestionsAction.PARAM_MORE;
import static org.sonar.server.component.ws.SuggestionsAction.PARAM_QUERY;
.containsExactly(tuple(project.getKey(), organization.getKey()));
}
+ @Test
+ public void must_not_search_if_no_valid_tokens_are_provided() throws Exception {
+ ComponentDto project = db.components().insertComponent(newProjectDto(organization).setName("SonarQube"));
+
+ componentIndexer.indexOnStartup(null);
+ authorizationIndexerTester.allowOnlyAnyone(project);
+
+ SuggestionsWsResponse response = actionTester.newRequest()
+ .setMethod("POST")
+ .setParam(PARAM_QUERY, "S o")
+ .executeProtobuf(SuggestionsWsResponse.class);
+
+ assertThat(response.getResultsList()).filteredOn(q -> q.getItemsCount() > 0).isEmpty();
+ assertThat(response.getWarning()).contains(SHORT_INPUT_WARNING);
+ }
+
+ @Test
+ public void should_warn_about_short_inputs() throws Exception {
+ SuggestionsWsResponse response = actionTester.newRequest()
+ .setMethod("POST")
+ .setParam(PARAM_QUERY, "validLongToken x")
+ .executeProtobuf(SuggestionsWsResponse.class);
+
+ assertThat(response.getWarning()).contains(SHORT_INPUT_WARNING);
+ }
+
@Test
public void should_propose_to_show_more_results_if_7_projects_are_found() throws Exception {
check_proposal_to_show_more_results(7, DEFAULT_LIMIT, 1L, null);