From 027e5416b7556ccd3bccce80f91b93b5d1812f0a Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Tue, 18 Apr 2017 10:58:02 +0200 Subject: SONAR-9079 score favorite components higher in suggestions --- .../server/component/index/ComponentIndex.java | 18 +++--- .../component/index/ComponentIndexQuery.java | 12 ++++ .../component/index/ComponentIndexResults.java | 66 ++++++++++++++++++++++ .../org/sonar/server/component/ws/AppAction.java | 6 +- .../sonar/server/component/ws/ComponentsWs.java | 11 ++-- .../server/component/ws/SuggestionsAction.java | 41 ++++++++------ .../es/textsearch/ComponentTextSearchFeature.java | 27 ++++++++- .../ComponentTextSearchQueryFactory.java | 18 +++++- .../index/ComponentIndexCombinationTest.java | 2 +- .../index/ComponentIndexFeatureFavoriteTest.java | 58 +++++++++++++++++++ .../ComponentIndexFeatureRecentlyBrowsedTest.java | 2 +- .../index/ComponentIndexHighlightTest.java | 4 +- .../component/index/ComponentIndexScoreTest.java | 23 ++++++++ .../server/component/index/ComponentIndexTest.java | 3 +- .../sonar/server/component/ws/AppActionTest.java | 40 ++++++++----- .../server/component/ws/ComponentsWsTest.java | 58 +++++++------------ .../server/component/ws/SuggestionsActionTest.java | 31 +++++++++- 17 files changed, 320 insertions(+), 100 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexResults.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureFavoriteTest.java (limited to 'server') diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndex.java b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndex.java index 5ee71624704..f23a1c0094c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndex.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndex.java @@ -22,7 +22,6 @@ package org.sonar.server.component.index; import com.google.common.annotations.VisibleForTesting; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.stream.Stream; import org.elasticsearch.action.search.SearchRequestBuilder; @@ -38,7 +37,6 @@ import org.elasticsearch.search.aggregations.bucket.filters.InternalFilters.Buck import org.elasticsearch.search.aggregations.metrics.tophits.InternalTopHits; import org.elasticsearch.search.aggregations.metrics.tophits.TopHitsBuilder; import org.elasticsearch.search.highlight.HighlightBuilder; -import org.sonar.core.util.stream.MoreCollectors; import org.sonar.server.es.EsClient; import org.sonar.server.es.textsearch.ComponentTextSearchFeature; import org.sonar.server.es.textsearch.ComponentTextSearchQueryFactory; @@ -79,15 +77,15 @@ public class ComponentIndex { return field; } - public List search(ComponentIndexQuery query) { + public ComponentIndexResults search(ComponentIndexQuery query) { return search(query, ComponentTextSearchFeature.values()); } @VisibleForTesting - List search(ComponentIndexQuery query, ComponentTextSearchFeature... features) { + ComponentIndexResults search(ComponentIndexQuery query, ComponentTextSearchFeature... features) { Collection qualifiers = query.getQualifiers(); if (qualifiers.isEmpty()) { - return Collections.emptyList(); + return ComponentIndexResults.newBuilder().build(); } SearchRequestBuilder request = client @@ -128,16 +126,18 @@ public class ComponentIndex { .setFieldKey(FIELD_KEY) .setFieldName(FIELD_NAME) .setRecentlyBrowsedKeys(query.getRecentlyBrowsedKeys()) + .setFavoriteKeys(query.getFavoriteKeys()) .build(); return esQuery.must(ComponentTextSearchQueryFactory.createQuery(componentTextSearchQuery, features)); } - private static List aggregationsToQualifiers(SearchResponse response) { + private static ComponentIndexResults aggregationsToQualifiers(SearchResponse response) { InternalFilters filtersAgg = response.getAggregations().get(FILTERS_AGGREGATION_NAME); List buckets = filtersAgg.getBuckets(); - return buckets.stream() - .map(ComponentIndex::bucketToQualifier) - .collect(MoreCollectors.toList(buckets.size())); + return ComponentIndexResults.newBuilder() + .setQualifiers( + buckets.stream().map(ComponentIndex::bucketToQualifier)) + .build(); } private static ComponentHitsPerQualifier bucketToQualifier(Bucket bucket) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexQuery.java b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexQuery.java index 1c2f05c3006..806a7ba796e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexQuery.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexQuery.java @@ -34,6 +34,7 @@ public class ComponentIndexQuery { private final String query; private final Collection qualifiers; private final Set recentlyBrowsedKeys; + private final Set favoriteKeys; @CheckForNull private final Integer limit; @@ -41,6 +42,7 @@ public class ComponentIndexQuery { this.query = requireNonNull(builder.query); this.qualifiers = requireNonNull(builder.qualifiers); this.recentlyBrowsedKeys = requireNonNull(builder.recentlyBrowsedKeys); + this.favoriteKeys = requireNonNull(builder.favoriteKeys); this.limit = builder.limit; } @@ -64,10 +66,15 @@ public class ComponentIndexQuery { return new Builder(); } + public Set getFavoriteKeys() { + return favoriteKeys; + } + public static class Builder { private String query; private Collection qualifiers = Collections.emptyList(); private Set recentlyBrowsedKeys = Collections.emptySet(); + private Set favoriteKeys = Collections.emptySet(); private Integer limit; private Builder() { @@ -89,6 +96,11 @@ public class ComponentIndexQuery { return this; } + public Builder setFavoriteKeys(Set favoriteKeys) { + this.favoriteKeys = Collections.unmodifiableSet(favoriteKeys); + return this; + } + public Builder setLimit(@Nullable Integer limit) { checkArgument(limit == null || limit > 0, "Limit has to be strictly positive: %s", limit); this.limit = limit; diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexResults.java b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexResults.java new file mode 100644 index 00000000000..06234cc10c5 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexResults.java @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.sonar.server.component.index; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Collections.emptyList; +import static java.util.Objects.requireNonNull; + +public class ComponentIndexResults { + + private final List qualifiers; + + private ComponentIndexResults(Builder builder) { + this.qualifiers = requireNonNull(builder.qualifiers); + } + + public Stream getQualifiers() { + return qualifiers.stream(); + } + + public boolean isEmpty() { + return qualifiers.isEmpty(); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + + private List qualifiers = emptyList(); + + private Builder() { + } + + public Builder setQualifiers(Stream qualifiers) { + this.qualifiers = qualifiers.collect(Collectors.toList()); + return this; + } + + public ComponentIndexResults build() { + return new ComponentIndexResults(this); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java index fa07c2e8920..7b423940673 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java @@ -30,7 +30,6 @@ import org.apache.commons.lang.BooleanUtils; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Metric; import org.sonar.api.server.ws.Request; -import org.sonar.api.server.ws.RequestHandler; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.text.JsonWriter; @@ -51,7 +50,7 @@ import static com.google.common.collect.Lists.newArrayList; import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; -public class AppAction implements RequestHandler { +public class AppAction implements ComponentsWsAction { private static final String PARAM_COMPONENT_ID = "componentId"; private static final String PARAM_COMPONENT = "component"; @@ -71,7 +70,8 @@ public class AppAction implements RequestHandler { this.componentFinder = componentFinder; } - void define(WebService.NewController controller) { + @Override + public void define(WebService.NewController controller) { WebService.NewAction action = controller.createAction("app") .setDescription("Coverage data required for rendering the component viewer.
" + "Requires the following permission: 'Browse'.") diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWs.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWs.java index 0ff209740c6..907b7159250 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWs.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWs.java @@ -19,17 +19,16 @@ */ package org.sonar.server.component.ws; +import java.util.Arrays; import org.sonar.api.server.ws.WebService; import static org.sonarqube.ws.client.component.ComponentsWsParameters.CONTROLLER_COMPONENTS; public class ComponentsWs implements WebService { - private final AppAction appAction; private final ComponentsWsAction[] actions; - public ComponentsWs(AppAction appAction, ComponentsWsAction... actions) { - this.appAction = appAction; + public ComponentsWs(ComponentsWsAction... actions) { this.actions = actions; } @@ -40,10 +39,8 @@ public class ComponentsWs implements WebService { .setDescription("Get information about a component (file, directory, project, ...) and its ancestors or descendants. " + "Update a project or module key."); - for (ComponentsWsAction action : actions) { - action.define(controller); - } - appAction.define(controller); + Arrays.stream(actions) + .forEach(action -> action.define(controller)); controller.done(); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java index 8db06733b45..244fb4f956e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java @@ -40,7 +40,9 @@ import org.sonar.server.component.index.ComponentHit; import org.sonar.server.component.index.ComponentHitsPerQualifier; import org.sonar.server.component.index.ComponentIndex; import org.sonar.server.component.index.ComponentIndexQuery; +import org.sonar.server.component.index.ComponentIndexResults; import org.sonar.server.es.textsearch.ComponentTextSearchFeature; +import org.sonar.server.favorite.FavoriteFinder; import org.sonarqube.ws.WsComponents.SuggestionsWsResponse; import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Category; import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Project; @@ -71,12 +73,14 @@ public class SuggestionsAction implements ComponentsWsAction { static final int EXTENDED_LIMIT = 20; private final ComponentIndex index; + private final FavoriteFinder favoriteFinder; private DbClient dbClient; - public SuggestionsAction(DbClient dbClient, ComponentIndex index) { + public SuggestionsAction(DbClient dbClient, ComponentIndex index, FavoriteFinder favoriteFinder) { this.dbClient = dbClient; this.index = index; + this.favoriteFinder = favoriteFinder; } @Override @@ -120,13 +124,17 @@ public class SuggestionsAction implements ComponentsWsAction { String more = wsRequest.param(PARAM_MORE); List recentlyBrowsedParam = wsRequest.paramAsStrings(PARAM_RECENTLY_BROWSED); Set recentlyBrowsedKeys = recentlyBrowsedParam == null ? emptySet() : copyOf(recentlyBrowsedParam); + Set favoriteKeys = favoriteFinder.list().stream().map(ComponentDto::getKey).collect(Collectors.toSet()); - ComponentIndexQuery.Builder queryBuilder = ComponentIndexQuery.builder().setQuery(query).setRecentlyBrowsedKeys(recentlyBrowsedKeys); + ComponentIndexQuery.Builder queryBuilder = ComponentIndexQuery.builder() + .setQuery(query) + .setRecentlyBrowsedKeys(recentlyBrowsedKeys) + .setFavoriteKeys(favoriteKeys); - List componentsPerQualifiers = getComponentsPerQualifiers(more, queryBuilder); + ComponentIndexResults componentsPerQualifiers = getComponentsPerQualifiers(more, queryBuilder); String warning = getWarning(query); - SuggestionsWsResponse searchWsResponse = toResponse(componentsPerQualifiers, recentlyBrowsedKeys, warning); + SuggestionsWsResponse searchWsResponse = toResponse(componentsPerQualifiers, recentlyBrowsedKeys, favoriteKeys, warning); writeProtobuf(searchWsResponse, wsRequest, wsResponse); } @@ -138,8 +146,7 @@ public class SuggestionsAction implements ComponentsWsAction { return null; } - private List getComponentsPerQualifiers(String more, ComponentIndexQuery.Builder queryBuilder) { - List componentsPerQualifiers; + private ComponentIndexResults getComponentsPerQualifiers(@Nullable String more, ComponentIndexQuery.Builder queryBuilder) { if (more == null) { queryBuilder.setQualifiers(stream(SuggestionCategory.values()).map(SuggestionCategory::getQualifier).collect(Collectors.toList())) .setLimit(DEFAULT_LIMIT); @@ -147,22 +154,21 @@ public class SuggestionsAction implements ComponentsWsAction { queryBuilder.setQualifiers(singletonList(SuggestionCategory.getByName(more).getQualifier())) .setLimit(EXTENDED_LIMIT); } - componentsPerQualifiers = searchInIndex(queryBuilder.build()); - return componentsPerQualifiers; + return searchInIndex(queryBuilder.build()); } - private List searchInIndex(ComponentIndexQuery componentIndexQuery) { + private ComponentIndexResults searchInIndex(ComponentIndexQuery componentIndexQuery) { return index.search(componentIndexQuery); } - private SuggestionsWsResponse toResponse(List componentsPerQualifiers, Set recentlyBrowsedKeys, @Nullable String warning) { + private SuggestionsWsResponse toResponse(ComponentIndexResults componentsPerQualifiers, Set recentlyBrowsedKeys, Set favoriteKeys, @Nullable String warning) { SuggestionsWsResponse.Builder builder = newBuilder(); if (!componentsPerQualifiers.isEmpty()) { Map organizationsByUuids; Map componentsByUuids; Map projectsByUuids; try (DbSession dbSession = dbClient.openSession(false)) { - Set componentUuids = componentsPerQualifiers.stream() + Set componentUuids = componentsPerQualifiers.getQualifiers() .map(ComponentHitsPerQualifier::getHits) .flatMap(Collection::stream) .map(ComponentHit::getUuid) @@ -182,7 +188,7 @@ public class SuggestionsAction implements ComponentsWsAction { .collect(MoreCollectors.uniqueIndex(ComponentDto::uuid)); } builder - .addAllSuggestions(toCategories(componentsPerQualifiers, recentlyBrowsedKeys, componentsByUuids, organizationsByUuids, projectsByUuids)) + .addAllSuggestions(toCategories(componentsPerQualifiers, recentlyBrowsedKeys, favoriteKeys, componentsByUuids, organizationsByUuids, projectsByUuids)) .addAllOrganizations(toOrganizations(organizationsByUuids)) .addAllProjects(toProjects(projectsByUuids)); } @@ -190,12 +196,12 @@ public class SuggestionsAction implements ComponentsWsAction { return builder.build(); } - private static List toCategories(List componentsPerQualifiers, Set recentlyBrowsedKeys, Map componentsByUuids, - Map organizationByUuids, Map projectsByUuids) { - return componentsPerQualifiers.stream().map(qualifier -> { + private static List toCategories(ComponentIndexResults componentsPerQualifiers, Set recentlyBrowsedKeys, Set favoriteKeys, + Map componentsByUuids, Map organizationByUuids, Map projectsByUuids) { + return componentsPerQualifiers.getQualifiers().map(qualifier -> { List suggestions = qualifier.getHits().stream() - .map(hit -> toSuggestion(hit, recentlyBrowsedKeys, componentsByUuids, organizationByUuids, projectsByUuids)) + .map(hit -> toSuggestion(hit, recentlyBrowsedKeys, favoriteKeys, componentsByUuids, organizationByUuids, projectsByUuids)) .collect(toList()); return Category.newBuilder() @@ -206,7 +212,7 @@ public class SuggestionsAction implements ComponentsWsAction { }).collect(toList()); } - private static Suggestion toSuggestion(ComponentHit hit, Set recentlyBrowsedKeys, Map componentsByUuids, + private static Suggestion toSuggestion(ComponentHit hit, Set recentlyBrowsedKeys, Set favoriteKeys, Map componentsByUuids, Map organizationByUuids, Map projectsByUuids) { ComponentDto result = componentsByUuids.get(hit.getUuid()); String organizationKey = organizationByUuids.get(result.getOrganizationUuid()).getKey(); @@ -219,6 +225,7 @@ public class SuggestionsAction implements ComponentsWsAction { .setName(result.longName()) .setMatch(hit.getHighlightedText().orElse(HtmlEscapers.htmlEscaper().escape(result.longName()))) .setIsRecentlyBrowsed(recentlyBrowsedKeys.contains(result.getKey())) + .setIsFavorite(favoriteKeys.contains(result.getKey())) .build(); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchFeature.java b/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchFeature.java index 86aa7e9b57e..980bc088332 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchFeature.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchFeature.java @@ -21,6 +21,7 @@ package org.sonar.server.es.textsearch; import java.util.Arrays; import java.util.Locale; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; import org.apache.commons.lang.StringUtils; @@ -83,12 +84,32 @@ public enum ComponentTextSearchFeature { }, RECENTLY_BROWSED { @Override - public QueryBuilder getQuery(ComponentTextSearchQuery query) { - return termsQuery(query.getFieldKey(), query.getRecentlyBrowsedKeys()).boost(100f); + public Stream getQueries(ComponentTextSearchQuery query) { + Set recentlyBrowsedKeys = query.getRecentlyBrowsedKeys(); + if (recentlyBrowsedKeys.isEmpty()) { + return Stream.empty(); + } + return Stream.of(termsQuery(query.getFieldKey(), recentlyBrowsedKeys).boost(100f)); + } + }, + FAVORITE { + @Override + public Stream getQueries(ComponentTextSearchQuery query) { + Set favoriteKeys = query.getFavoriteKeys(); + if (favoriteKeys.isEmpty()) { + return Stream.empty(); + } + return Stream.of(termsQuery(query.getFieldKey(), favoriteKeys).boost(1000f)); } }; - public abstract QueryBuilder getQuery(ComponentTextSearchQuery query); + public Stream getQueries(ComponentTextSearchQuery query) { + return Stream.of(getQuery(query)); + } + + public QueryBuilder getQuery(ComponentTextSearchQuery query) { + throw new UnsupportedOperationException(); + } public static Stream split(String queryText) { return Arrays.stream( diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchQueryFactory.java b/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchQueryFactory.java index 46c7ec84f4e..6a205fbdd73 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchQueryFactory.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchQueryFactory.java @@ -45,7 +45,7 @@ public class ComponentTextSearchQueryFactory { checkArgument(features.length > 0, "features cannot be empty"); BoolQueryBuilder featureQuery = boolQuery(); Arrays.stream(features) - .map(f -> f.getQuery(query)) + .flatMap(f -> f.getQueries(query)) .forEach(featureQuery::should); return featureQuery; } @@ -55,12 +55,14 @@ public class ComponentTextSearchQueryFactory { private final String fieldKey; private final String fieldName; private final Set recentlyBrowsedKeys; + private final Set favoriteKeys; private ComponentTextSearchQuery(Builder builder) { this.queryText = builder.queryText; this.fieldKey = builder.fieldKey; this.fieldName = builder.fieldName; this.recentlyBrowsedKeys = builder.recentlyBrowsedKeys; + this.favoriteKeys = builder.favoriteKeys; } public String getQueryText() { @@ -83,11 +85,16 @@ public class ComponentTextSearchQueryFactory { return new Builder(); } + public Set getFavoriteKeys() { + return favoriteKeys; + } + public static class Builder { private String queryText; private String fieldKey; private String fieldName; private Set recentlyBrowsedKeys = Collections.emptySet(); + private Set favoriteKeys = Collections.emptySet(); /** * The text search query @@ -121,11 +128,20 @@ public class ComponentTextSearchQueryFactory { return this; } + /** + * Component keys of favorite items + */ + public Builder setFavoriteKeys(Set favoriteKeys) { + this.favoriteKeys = ImmutableSet.copyOf(favoriteKeys); + return this; + } + public ComponentTextSearchQuery build() { this.queryText = requireNonNull(queryText, "query text cannot be null"); this.fieldKey = requireNonNull(fieldKey, "field key cannot be null"); this.fieldName = requireNonNull(fieldName, "field name cannot be null"); this.recentlyBrowsedKeys = requireNonNull(recentlyBrowsedKeys, "field recentlyBrowsedKeys cannot be null"); + this.favoriteKeys = requireNonNull(favoriteKeys, "field favoriteKeys cannot be null"); return new ComponentTextSearchQuery(this); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexCombinationTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexCombinationTest.java index 7f444b064fe..56b82d6730d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexCombinationTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexCombinationTest.java @@ -33,7 +33,7 @@ public class ComponentIndexCombinationTest extends ComponentIndexTest { public void return_empty_list_if_no_fields_match_query() { indexProject("struts", "Apache Struts"); - assertThat(index.search(ComponentIndexQuery.builder().setQuery("missing").build())).isEmpty(); + assertThat(index.search(ComponentIndexQuery.builder().setQuery("missing").build()).isEmpty()).isTrue(); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureFavoriteTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureFavoriteTest.java new file mode 100644 index 00000000000..76fa97b3724 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureFavoriteTest.java @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.sonar.server.component.index; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.es.textsearch.ComponentTextSearchFeature; + +import static com.google.common.collect.ImmutableSet.of; +import static java.util.Collections.singletonList; +import static org.sonar.api.resources.Qualifiers.PROJECT; + +public class ComponentIndexFeatureFavoriteTest extends ComponentIndexTest { + + @Before + public void before() { + features.set(ComponentTextSearchFeature.PREFIX, ComponentTextSearchFeature.FAVORITE); + } + + @Test + public void scoring_cares_about_favorites() { + ComponentDto project1 = indexProject("sonarqube", "SonarQube"); + ComponentDto project2 = indexProject("recent", "SonarQube Recently"); + + ComponentIndexQuery query1 = ComponentIndexQuery.builder() + .setQuery("SonarQube") + .setQualifiers(singletonList(PROJECT)) + .setFavoriteKeys(of(project1.getKey())) + .build(); + assertSearch(query1).containsExactly(uuids(project1, project2)); + + ComponentIndexQuery query2 = ComponentIndexQuery.builder() + .setQuery("SonarQube") + .setQualifiers(singletonList(PROJECT)) + .setFavoriteKeys(of(project2.getKey())) + .build(); + assertSearch(query2).containsExactly(uuids(project2, project1)); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureRecentlyBrowsedTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureRecentlyBrowsedTest.java index 5ab1ae5561a..e6f33e594c1 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureRecentlyBrowsedTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureRecentlyBrowsedTest.java @@ -33,7 +33,7 @@ public class ComponentIndexFeatureRecentlyBrowsedTest extends ComponentIndexTest @Before public void before() { - features.set(ComponentTextSearchFeature.PREFIX, ComponentTextSearchFeature.EXACT_IGNORE_CASE, ComponentTextSearchFeature.RECENTLY_BROWSED); + features.set(ComponentTextSearchFeature.PREFIX, ComponentTextSearchFeature.RECENTLY_BROWSED); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexHighlightTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexHighlightTest.java index 1f0c14fa483..97f81c171eb 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexHighlightTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexHighlightTest.java @@ -20,8 +20,8 @@ package org.sonar.server.component.index; import java.util.Collections; -import java.util.List; import java.util.Optional; +import java.util.stream.Stream; import org.junit.Test; import org.sonar.api.resources.Qualifiers; @@ -66,7 +66,7 @@ public class ComponentIndexHighlightTest extends ComponentIndexTest { .setQuery(search) .setQualifiers(Collections.singletonList(Qualifiers.FILE)) .build(); - List results = index.search(query, features.get()); + Stream results = index.search(query, features.get()).getQualifiers(); assertThat(results).flatExtracting(ComponentHitsPerQualifier::getHits) .extracting(ComponentHit::getHighlightedText) diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexScoreTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexScoreTest.java index bad8ea15400..6cc39ae4877 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexScoreTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexScoreTest.java @@ -19,11 +19,17 @@ */ package org.sonar.server.component.index; +import com.google.common.collect.ImmutableSet; import org.junit.Test; import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentTesting; import org.sonar.server.es.textsearch.ComponentTextSearchFeature; +import static java.util.Arrays.asList; +import static org.sonar.api.resources.Qualifiers.FILE; +import static org.sonar.api.resources.Qualifiers.MODULE; +import static org.sonar.api.resources.Qualifiers.PROJECT; + public class ComponentIndexScoreTest extends ComponentIndexTest { @Test @@ -104,6 +110,23 @@ public class ComponentIndexScoreTest extends ComponentIndexTest { "SonarQube"); } + @Test + public void should_prefer_favorite_over_recently_browsed() { + ComponentDto recentlyBrowsed = db.components().insertProject(c -> c.setName("File1")); + index(recentlyBrowsed); + + ComponentDto favorite = db.components().insertProject(c -> c.setName("File2")); + index(favorite); + + ComponentIndexQuery query = ComponentIndexQuery.builder() + .setQuery("sonarqube") + .setQualifiers(asList(PROJECT, MODULE, FILE)) + .setRecentlyBrowsedKeys(ImmutableSet.of(recentlyBrowsed.getKey())) + .setFavoriteKeys(ImmutableSet.of(favorite.getKey())) + .build(); + assertSearch(query).containsExactly(uuids(favorite, recentlyBrowsed)); + } + @Test public void do_not_match_wrong_file_extension() { ComponentDto file1 = indexFile("MyClass.java"); diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexTest.java index b483f6c7752..f333da67d33 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexTest.java @@ -60,7 +60,6 @@ public abstract class ComponentIndexTest { public ComponentTextSearchFeatureRule features = new ComponentTextSearchFeatureRule(); protected ComponentIndexer indexer = new ComponentIndexer(db.getDbClient(), es.client()); - protected ComponentIndex index = new ComponentIndex(es.client(), new AuthorizationTypeSupport(userSession)); protected PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, indexer); private OrganizationDto organization; @@ -103,7 +102,7 @@ public abstract class ComponentIndexTest { } protected AbstractListAssert, String> assertSearch(ComponentIndexQuery query) { - return assertThat(index.search(query, features.get())) + return assertThat(index.search(query, features.get()).getQualifiers()) .flatExtracting(ComponentHitsPerQualifier::getHits) .extracting(ComponentHit::getUuid); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/AppActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/AppActionTest.java index c978c2d2042..347565c86da 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/AppActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/AppActionTest.java @@ -25,6 +25,7 @@ import javax.annotation.Nullable; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.System2; import org.sonar.api.web.UserRole; import org.sonar.db.DbTester; @@ -38,9 +39,11 @@ import org.sonar.db.metric.MetricDto; import org.sonar.server.component.ComponentFinder; import org.sonar.server.startup.RegisterMetrics; import org.sonar.server.tester.UserSessionRule; -import org.sonar.server.ws.WsTester; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; +import org.sonar.test.JsonAssert; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY; import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY_KEY; import static org.sonar.api.measures.CoreMetrics.LINES_KEY; @@ -67,15 +70,12 @@ public class AppActionTest { @Rule public DbTester dbTester = DbTester.create(System2.INSTANCE); - private WsTester wsTester; + private AppAction underTest = new AppAction(dbTester.getDbClient(), userSessionRule, new ComponentFinder(dbTester.getDbClient())); + private WsActionTester wsTester = new WsActionTester(underTest); @Before public void setUp() { insertMetrics(); - wsTester = new WsTester( - new ComponentsWs( - new AppAction(dbTester.getDbClient(), userSessionRule, new ComponentFinder(dbTester.getDbClient())), - mock(SuggestionsAction.class))); } @Test @@ -84,8 +84,8 @@ public class AppActionTest { dbTester.commit(); userSessionRule.logIn("john").addComponentUuidPermission(UserRole.USER, PROJECT_UUID, FILE_UUID); - WsTester.TestRequest request = wsTester.newGetRequest("api/components", "app").setParam("uuid", FILE_UUID); - request.execute().assertJson(getClass(), "app.json"); + TestRequest request = wsTester.newRequest().setParam("uuid", FILE_UUID); + jsonAssert(request, "app.json"); } @Test @@ -102,8 +102,8 @@ public class AppActionTest { userSessionRule .logIn("john") .addComponentUuidPermission(UserRole.USER, PROJECT_UUID, FILE_UUID); - WsTester.TestRequest request = wsTester.newGetRequest("api/components", "app").setParam("uuid", FILE_UUID); - request.execute().assertJson(getClass(), "app_with_measures.json"); + TestRequest request = wsTester.newRequest().setParam("uuid", FILE_UUID); + jsonAssert(request, "app_with_measures.json"); } @Test @@ -113,8 +113,18 @@ public class AppActionTest { dbTester.commit(); userSessionRule.logIn("john").addComponentUuidPermission(UserRole.USER, PROJECT_UUID, FILE_UUID); - WsTester.TestRequest request = wsTester.newGetRequest("api/components", "app").setParam("uuid", FILE_UUID); - request.execute().assertJson(getClass(), "app_with_ut_measure.json"); + TestRequest request = wsTester.newRequest().setParam("uuid", FILE_UUID); + jsonAssert(request, "app_with_ut_measure.json"); + } + + @Test + public void define_app_action() { + WebService.Action action = wsTester.getDef(); + assertThat(action).isNotNull(); + assertThat(action.isInternal()).isTrue(); + assertThat(action.isPost()).isFalse(); + assertThat(action.handler()).isNotNull(); + assertThat(action.params()).hasSize(3); } private void insertMetrics() { @@ -153,4 +163,8 @@ public class AppActionTest { .setData(data); dbTester.getDbClient().measureDao().insert(dbTester.getSession(), measure); } + + private void jsonAssert(TestRequest request, String filename) { + JsonAssert.assertJson(request.execute().getInput()).isSimilarTo(getClass().getResource(getClass().getSimpleName()+"/"+ filename)); + } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsTest.java index 73db6132f2a..7fab77c7c15 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsTest.java @@ -19,59 +19,39 @@ */ package org.sonar.server.component.ws; -import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.sonar.api.i18n.I18n; -import org.sonar.api.resources.Language; -import org.sonar.api.resources.Languages; -import org.sonar.api.resources.ResourceTypes; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; -import org.sonar.db.DbClient; -import org.sonar.server.component.ComponentFinder; -import org.sonar.server.component.index.ComponentIndex; -import org.sonar.server.organization.TestDefaultOrganizationProvider; -import org.sonar.server.tester.UserSessionRule; -import org.sonar.server.ws.WsTester; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.RETURNS_DEEP_STUBS; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.CONTROLLER_COMPONENTS; public class ComponentsWsTest { - @Rule - public UserSessionRule userSessionRule = UserSessionRule.standalone(); - private WebService.Controller controller; + private String actionKey = randomAlphanumeric(10); + private ComponentsWsAction action = new ComponentsWsAction() { - @Before - public void setUp() { - Languages languages = mock(Languages.class, RETURNS_DEEP_STUBS); - when(languages.all()).thenReturn(new Language[0]); + @Override + public void handle(Request request, Response response) throws Exception { + } - WsTester tester = new WsTester(new ComponentsWs( - new AppAction(mock(DbClient.class), userSessionRule, mock(ComponentFinder.class)), - new SuggestionsAction(mock(DbClient.class), mock(ComponentIndex.class)), - new SearchAction(mock(DbClient.class), mock(ResourceTypes.class), mock(I18n.class), userSessionRule, languages, TestDefaultOrganizationProvider.fromUuid("foo")))); - controller = tester.controller("api/components"); - } + @Override + public void define(WebService.NewController context) { + context.createAction(actionKey).setHandler(this); + } + }; @Test public void define_controller() { + WebService.Context context = new WebService.Context(); + new ComponentsWs(action).define(context); + WebService.Controller controller = context.controller(CONTROLLER_COMPONENTS); + assertThat(controller).isNotNull(); assertThat(controller.description()).isNotEmpty(); assertThat(controller.since()).isEqualTo("4.2"); - assertThat(controller.actions()).hasSize(3); - } - - @Test - public void define_app_action() { - WebService.Action action = controller.action("app"); - assertThat(action).isNotNull(); - assertThat(action.isInternal()).isTrue(); - assertThat(action.isPost()).isFalse(); - assertThat(action.handler()).isNotNull(); - assertThat(action.params()).hasSize(3); + assertThat(controller.actions()).extracting(WebService.Action::key).containsExactly(actionKey); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java index 2b0233b0f62..71b3a73d7e5 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java @@ -39,6 +39,7 @@ import org.sonar.server.component.index.ComponentIndexDefinition; import org.sonar.server.component.index.ComponentIndexer; import org.sonar.server.es.EsTester; import org.sonar.server.es.ProjectIndexer; +import org.sonar.server.favorite.FavoriteFinder; import org.sonar.server.permission.index.AuthorizationTypeSupport; import org.sonar.server.permission.index.PermissionIndexerTester; import org.sonar.server.tester.UserSessionRule; @@ -48,12 +49,15 @@ import org.sonarqube.ws.WsComponents.SuggestionsWsResponse; import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Project; import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Suggestion; +import static java.util.Collections.singletonList; import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.joining; import static java.util.stream.IntStream.range; import static java.util.stream.Stream.of; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.sonar.db.component.ComponentTesting.newModuleDto; import static org.sonar.db.component.ComponentTesting.newProjectDto; import static org.sonar.server.component.ws.SuggestionsAction.DEFAULT_LIMIT; @@ -64,7 +68,6 @@ import static org.sonar.server.component.ws.SuggestionsAction.PARAM_QUERY; import static org.sonar.server.component.ws.SuggestionsAction.PARAM_RECENTLY_BROWSED; import static org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Category; import static org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Organization; -import static org.sonarqube.ws.WsComponents.SuggestionsWsResponse.parseFrom; public class SuggestionsActionTest { @@ -76,8 +79,9 @@ public class SuggestionsActionTest { public UserSessionRule userSessionRule = UserSessionRule.standalone(); private ComponentIndexer componentIndexer = new ComponentIndexer(db.getDbClient(), es.client()); + private FavoriteFinder favoriteFinder = mock(FavoriteFinder.class); private ComponentIndex index = new ComponentIndex(es.client(), new AuthorizationTypeSupport(userSessionRule)); - private SuggestionsAction underTest = new SuggestionsAction(db.getDbClient(), index); + private SuggestionsAction underTest = new SuggestionsAction(db.getDbClient(), index, favoriteFinder); private OrganizationDto organization; private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, componentIndexer); private WsActionTester actionTester = new WsActionTester(underTest); @@ -229,6 +233,29 @@ public class SuggestionsActionTest { .containsExactly(true, false); } + @Test + public void should_mark_favorite_items() throws Exception { + ComponentDto project = db.components().insertComponent(newProjectDto(organization)); + ComponentDto favorite = newModuleDto(project).setName("Module1"); + db.components().insertComponent(favorite); + doReturn(singletonList(favorite)).when(favoriteFinder).list(); + + ComponentDto nonFavorite = newModuleDto(project).setName("Module2"); + db.components().insertComponent(nonFavorite); + componentIndexer.indexProject(project.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION); + authorizationIndexerTester.allowOnlyAnyone(project); + + SuggestionsWsResponse response = actionTester.newRequest() + .setMethod("POST") + .setParam(PARAM_QUERY, "Module") + .executeProtobuf(SuggestionsWsResponse.class); + + assertThat(response.getSuggestionsList()) + .flatExtracting(Category::getSuggestionsList) + .extracting(Suggestion::getKey, Suggestion::getIsFavorite) + .containsExactly(tuple(favorite.getKey(), true), tuple(nonFavorite.getKey(), false)); + } + @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); -- cgit v1.2.3