Browse Source

SONAR-7282 implement global search with elasticsearch

Change api/component/suggestions.
The index is not yet cleaned up if a project's content changes or a project is deleted.
Delete previous ruby implementation of the webservice.
tags/6.3-RC1
Daniel Schwarz 7 years ago
parent
commit
99edd870a8
36 changed files with 1110 additions and 162 deletions
  1. 2
    0
      server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
  2. 1
    1
      server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
  3. 13
    6
      server/sonar-server/src/main/java/org/sonar/server/component/ComponentService.java
  4. 86
    0
      server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentDoc.java
  5. 77
    0
      server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndex.java
  6. 60
    0
      server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexDefinition.java
  7. 68
    0
      server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexQuery.java
  8. 121
    0
      server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java
  9. 23
    0
      server/sonar-server/src/main/java/org/sonar/server/component/index/package-info.java
  10. 0
    19
      server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWs.java
  11. 1
    0
      server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWsModule.java
  12. 147
    0
      server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java
  13. 7
    5
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/IndexComponentsStep.java
  14. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/es/DefaultIndexSettings.java
  15. 9
    2
      server/sonar-server/src/main/java/org/sonar/server/es/IndexerStartupTask.java
  16. 6
    0
      server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
  17. 0
    5
      server/sonar-server/src/main/resources/org/sonar/server/component/ws/components-example-suggestions.json
  18. 5
    3
      server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceTest.java
  19. 3
    1
      server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java
  20. 4
    3
      server/sonar-server/src/test/java/org/sonar/server/component/DefaultRubyComponentServiceTest.java
  21. 203
    0
      server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexTest.java
  22. 125
    0
      server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexerTest.java
  23. 5
    2
      server/sonar-server/src/test/java/org/sonar/server/component/ws/AppActionTest.java
  24. 5
    2
      server/sonar-server/src/test/java/org/sonar/server/component/ws/BulkUpdateKeyActionTest.java
  25. 1
    1
      server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsModuleTest.java
  26. 5
    6
      server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsTest.java
  27. 6
    2
      server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchViewComponentsActionTest.java
  28. 90
    0
      server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java
  29. 4
    2
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/IndexComponentsStepTest.java
  30. 3
    1
      server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/AddProjectActionTest.java
  31. 0
    100
      server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/components_controller.rb
  32. 8
    0
      sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java
  33. 3
    0
      sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java
  34. 7
    0
      sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml
  35. 1
    0
      sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java
  36. 10
    0
      sonar-ws/src/main/protobuf/ws-components.proto

+ 2
- 0
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java View File

@@ -68,6 +68,7 @@ import org.sonar.process.logging.LogbackHelper;
import org.sonar.server.component.ComponentCleanerService;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.ComponentService;
import org.sonar.server.component.index.ComponentIndexer;
import org.sonar.server.computation.queue.PurgeCeActivities;
import org.sonar.server.computation.task.projectanalysis.ProjectAnalysisTaskModule;
import org.sonar.server.computation.taskprocessor.CeTaskProcessorModule;
@@ -340,6 +341,7 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {
NewAlerts.newMetadata(),
ComponentCleanerService.class,
ProjectMeasuresIndexer.class,
ComponentIndexer.class,

// views
ViewIndexer.class,

+ 1
- 1
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java View File

@@ -88,7 +88,7 @@ public class ComputeEngineContainerImplTest {
assertThat(picoContainer.getComponentAdapters())
.hasSize(
CONTAINER_ITSELF
+ 77 // level 4
+ 78 // level 4
+ 4 // content of CeConfigurationModule
+ 3 // content of CeHttpModule
+ 5 // content of CeQueueModule

+ 13
- 6
server/sonar-server/src/main/java/org/sonar/server/component/ComponentService.java View File

@@ -42,6 +42,7 @@ import org.sonar.core.util.Uuids;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
import org.sonar.server.component.index.ComponentIndexer;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.measure.index.ProjectMeasuresIndexer;
@@ -62,15 +63,17 @@ public class ComponentService {
private final System2 system2;
private final ComponentFinder componentFinder;
private final ProjectMeasuresIndexer projectMeasuresIndexer;
private final ComponentIndexer componentIndexer;

public ComponentService(DbClient dbClient, I18n i18n, UserSession userSession, System2 system2,
ComponentFinder componentFinder, ProjectMeasuresIndexer projectMeasuresIndexer) {
public ComponentService(DbClient dbClient, I18n i18n, UserSession userSession, System2 system2, ComponentFinder componentFinder, ProjectMeasuresIndexer projectMeasuresIndexer,
ComponentIndexer componentIndexer) {
this.dbClient = dbClient;
this.i18n = i18n;
this.userSession = userSession;
this.system2 = system2;
this.componentFinder = componentFinder;
this.projectMeasuresIndexer = projectMeasuresIndexer;
this.componentIndexer = componentIndexer;
}

public ComponentDto getByKey(String key) {
@@ -118,13 +121,13 @@ public class ComponentService {
checkProjectOrModuleKeyFormat(newKey);
dbClient.componentKeyUpdaterDao().updateKey(component.uuid(), newKey);
dbSession.commit();
projectMeasuresIndexer.index(component.uuid());
index(component.uuid());
}

public void bulkUpdateKey(DbSession dbSession, String projectUuid, String stringToReplace, String replacementString) {
dbClient.componentKeyUpdaterDao().bulkUpdateKey(dbSession, projectUuid, stringToReplace, replacementString);
dbSession.commit();
projectMeasuresIndexer.index(projectUuid);
index(projectUuid);
}

// Used by SQ and Governance
@@ -133,11 +136,15 @@ public class ComponentService {
checkKeyFormat(newComponent.qualifier(), newComponent.key());
ComponentDto rootComponent = createRootComponent(session, newComponent);
removeDuplicatedProjects(session, rootComponent.getKey());
projectMeasuresIndexer.index(rootComponent.uuid());

index(rootComponent.uuid());
return rootComponent;
}

private void index(String projectUuid) {
projectMeasuresIndexer.index(projectUuid);
componentIndexer.index(projectUuid);
}

/**
* No permission check must be done when inserting a new developer as it's done on Compute Engine side.
* No check must be done on the key

+ 86
- 0
server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentDoc.java View File

@@ -0,0 +1,86 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.HashMap;
import org.sonar.server.es.BaseDoc;

public class ComponentDoc extends BaseDoc {

public ComponentDoc() {
super(new HashMap<>(5));
}

@Override
public String getId() {
return getField("_id");
}

@Override
public String getRouting() {
return getField(ComponentIndexDefinition.FIELD_PROJECT_UUID);
}

@Override
public String getParent() {
return null;
}

public ComponentDoc setId(String s) {
setField("_id", s);
return this;
}

public String getProjectUuid() {
return getField(ComponentIndexDefinition.FIELD_PROJECT_UUID);
}

public ComponentDoc setProjectUuid(String s) {
setField(ComponentIndexDefinition.FIELD_PROJECT_UUID, s);
return this;
}

public String getKey() {
return getField(ComponentIndexDefinition.FIELD_KEY);
}

public ComponentDoc setKey(String s) {
setField(ComponentIndexDefinition.FIELD_KEY, s);
return this;
}

public String getName() {
return getField(ComponentIndexDefinition.FIELD_NAME);
}

public ComponentDoc setName(String s) {
setField(ComponentIndexDefinition.FIELD_NAME, s);
return this;
}

public String getQualifier() {
return getField(ComponentIndexDefinition.FIELD_QUALIFIER);
}

public ComponentDoc setQualifier(String s) {
setField(ComponentIndexDefinition.FIELD_QUALIFIER, s);
return this;
}
}

+ 77
- 0
server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndex.java View File

@@ -0,0 +1,77 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.Arrays;
import java.util.List;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.sonar.core.util.stream.Collectors;
import org.sonar.server.es.BaseIndex;
import org.sonar.server.es.EsClient;

import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
import static org.elasticsearch.index.query.QueryBuilders.termQuery;
import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_KEY;
import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_NAME;
import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_QUALIFIER;
import static org.sonar.server.component.index.ComponentIndexDefinition.INDEX_COMPONENTS;
import static org.sonar.server.component.index.ComponentIndexDefinition.TYPE_COMPONENT;

public class ComponentIndex extends BaseIndex {

public ComponentIndex(EsClient client) {
super(client);
}

public List<String> search(ComponentIndexQuery query) {
SearchRequestBuilder requestBuilder = getClient()
.prepareSearch(INDEX_COMPONENTS)
.setTypes(TYPE_COMPONENT)
.setFetchSource(false);

query.getLimit().ifPresent(requestBuilder::setSize);

requestBuilder.setQuery(
createQuery(query));

return Arrays.stream(requestBuilder.get().getHits().hits())
.map(SearchHit::getId)
.collect(Collectors.toList());
}

private static QueryBuilder createQuery(ComponentIndexQuery query) {
BoolQueryBuilder esQuery = boolQuery();

query
.getQualifiers()
.forEach(qualifier -> esQuery.filter(termQuery(FIELD_QUALIFIER, qualifier)));

return esQuery
.must(
boolQuery()
.should(matchQuery(FIELD_NAME + "." + SEARCH_PARTIAL_SUFFIX, query.getQuery()))
.should(termQuery(FIELD_KEY, query.getQuery()).boost(3f))
.minimumNumberShouldMatch(1));
}
}

+ 60
- 0
server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexDefinition.java View File

@@ -0,0 +1,60 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.sonar.api.config.Settings;
import org.sonar.server.es.IndexDefinition;
import org.sonar.server.es.NewIndex;

public class ComponentIndexDefinition implements IndexDefinition {

public static final String INDEX_COMPONENTS = "components";
public static final String TYPE_COMPONENT = "component";

public static final String FIELD_PROJECT_UUID = "project_uuid";
public static final String FIELD_KEY = "key";
public static final String FIELD_NAME = "name";
public static final String FIELD_QUALIFIER = "qualifier";

private static final int DEFAULT_NUMBER_OF_SHARDS = 5;

private final Settings settings;

public ComponentIndexDefinition(Settings settings) {
this.settings = settings;
}

@Override
public void define(IndexDefinitionContext context) {
NewIndex index = context.create(INDEX_COMPONENTS);
index.refreshHandledByIndexer();
index.configureShards(settings, DEFAULT_NUMBER_OF_SHARDS);

// type "component"
NewIndex.NewIndexType mapping = index.createType(TYPE_COMPONENT);
mapping.stringFieldBuilder(FIELD_PROJECT_UUID).build();
mapping.stringFieldBuilder(FIELD_KEY).build();
mapping.stringFieldBuilder(FIELD_NAME).enableGramSearch().build();
mapping.stringFieldBuilder(FIELD_QUALIFIER).build();

// do not store document but only indexation of information
mapping.setEnableSource(false);
}
}

+ 68
- 0
server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexQuery.java View File

@@ -0,0 +1,68 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

public class ComponentIndexQuery {

private final String query;
private Set<String> qualifiers = new HashSet<>();
private Optional<Integer> limit = Optional.empty();

public ComponentIndexQuery(String query) {
this.query = query;
}

public ComponentIndexQuery addQualifier(String qualifier) {
this.qualifiers.add(qualifier);
return this;
}

public ComponentIndexQuery addQualifiers(Collection<String> qualifiers) {
this.qualifiers.addAll(qualifiers);
return this;
}

public boolean hasQualifiers() {
return !qualifiers.isEmpty();
}

public Collection<String> getQualifiers() {
return Collections.unmodifiableSet(qualifiers);
}

public String getQuery() {
return query;
}

public ComponentIndexQuery setLimit(int limit) {
this.limit = Optional.of(limit);
return this;
}

public Optional<Integer> getLimit() {
return limit;
}
}

+ 121
- 0
server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java View File

@@ -0,0 +1,121 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 com.google.common.base.Throwables;
import com.google.common.util.concurrent.Uninterruptibles;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.elasticsearch.action.index.IndexRequest;
import org.sonar.api.Startable;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
import org.sonar.server.es.BulkIndexer;
import org.sonar.server.es.EsClient;

import static org.sonar.server.component.index.ComponentIndexDefinition.INDEX_COMPONENTS;
import static org.sonar.server.component.index.ComponentIndexDefinition.TYPE_COMPONENT;

public class ComponentIndexer implements Startable {

private final ThreadPoolExecutor executor;
private final DbClient dbClient;
private final EsClient esClient;

public ComponentIndexer(DbClient dbClient, EsClient esClient) {
this.executor = new ThreadPoolExecutor(0, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
this.dbClient = dbClient;
this.esClient = esClient;
}

/**
* Copy all database contents to the elastic search index.
*/
public void index() {
try (DbSession dbSession = dbClient.openSession(false)) {
dbClient.componentDao()
.selectAll(dbSession, context -> index((ComponentDto) context.getResultObject()));
}
}

/**
* Update the elastic search for one specific project. The current data from the database is used.
*/
public void index(String projectUuid) {
try (DbSession dbSession = dbClient.openSession(false)) {
index(dbClient.componentDao().selectByProjectUuid(projectUuid, dbSession).stream());
}
}

private void index(Stream<ComponentDto> components) {
components.forEach(this::index);
}

public void index(ComponentDto doc) {
index(toDocument(doc));
}

public void index(ComponentDoc document) {
Future<?> submit = executor.submit(() -> indexNow(document));
try {
Uninterruptibles.getUninterruptibly(submit);
} catch (ExecutionException e) {
Throwables.propagate(e);
}
}

private void indexNow(ComponentDoc doc) {
BulkIndexer bulk = new BulkIndexer(esClient, INDEX_COMPONENTS);
bulk.setLarge(false);
bulk.start();
bulk.add(newIndexRequest(doc));
bulk.stop();
}

private static IndexRequest newIndexRequest(ComponentDoc doc) {
return new IndexRequest(INDEX_COMPONENTS, TYPE_COMPONENT, doc.getId())
.source(doc.getFields());
}

private static ComponentDoc toDocument(ComponentDto component) {
return new ComponentDoc()
.setId(component.uuid())
.setName(component.name())
.setKey(component.key())
.setProjectUuid(component.projectUuid())
.setQualifier(component.qualifier());
}

@Override
public void start() {
// no action required, setup is done in constructor
}

@Override
public void stop() {
executor.shutdown();
}
}

+ 23
- 0
server/sonar-server/src/main/java/org/sonar/server/component/index/package-info.java View File

@@ -0,0 +1,23 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
@ParametersAreNonnullByDefault
package org.sonar.server.component.index;

import javax.annotation.ParametersAreNonnullByDefault;

+ 0
- 19
server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWs.java View File

@@ -19,8 +19,6 @@
*/
package org.sonar.server.component.ws;

import com.google.common.io.Resources;
import org.sonar.api.server.ws.RailsHandler;
import org.sonar.api.server.ws.WebService;

import static org.sonarqube.ws.client.component.ComponentsWsParameters.CONTROLLER_COMPONENTS;
@@ -49,25 +47,8 @@ public class ComponentsWs implements WebService {
}
appAction.define(controller);
searchViewComponentsAction.define(controller);
defineSuggestionsAction(controller);

controller.done();
}

private void defineSuggestionsAction(NewController controller) {
NewAction action = controller.createAction("suggestions")
.setDescription("Internal WS for the top-right search engine")
.setSince("4.2")
.setInternal(true)
.setHandler(RailsHandler.INSTANCE)
.setResponseExample(Resources.getResource(this.getClass(), "components-example-suggestions.json"));

action.createParam("s")
.setRequired(true)
.setDescription("Substring of project key (minimum 2 characters)")
.setExampleValue("sonar");

RailsHandler.addJsonOnlyFormatParam(action);
}

}

+ 1
- 0
server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWsModule.java View File

@@ -30,6 +30,7 @@ public class ComponentsWsModule extends Module {
// actions
AppAction.class,
SearchAction.class,
SuggestionsAction.class,
TreeAction.class,
ShowAction.class,
SearchViewComponentsAction.class,

+ 147
- 0
server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java View File

@@ -0,0 +1,147 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.ws;

import com.google.common.io.Resources;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.server.ws.WebService.NewAction;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
import org.sonar.server.component.index.ComponentIndex;
import org.sonar.server.component.index.ComponentIndexQuery;
import org.sonarqube.ws.WsComponents.Component;
import org.sonarqube.ws.WsComponents.SuggestionsWsResponse;
import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Qualifier;

import static org.sonar.server.ws.WsUtils.writeProtobuf;
import static org.sonarqube.ws.client.component.ComponentsWsParameters.ACTION_SUGGESTIONS;

public class SuggestionsAction implements ComponentsWsAction {

static final String URL_PARAM_QUERY = "s";

private static final String[] QUALIFIERS = {
Qualifiers.VIEW,
Qualifiers.SUBVIEW,
Qualifiers.PROJECT,
Qualifiers.MODULE,
Qualifiers.FILE,
Qualifiers.UNIT_TEST_FILE
};

private static final int NUMBER_OF_RESULTS_PER_QUALIFIER = 6;

private final ComponentIndex index;

private DbClient dbClient;

public SuggestionsAction(DbClient dbClient, ComponentIndex index) {
this.dbClient = dbClient;
this.index = index;
}

@Override
public void define(WebService.NewController context) {
NewAction action = context.createAction(ACTION_SUGGESTIONS)
.setDescription("Internal WS for the top-right search engine")
.setSince("4.2")
.setInternal(true)
.setHandler(this)
.setResponseExample(Resources.getResource(this.getClass(), "components-example-suggestions.json"));

action.createParam(URL_PARAM_QUERY)
.setRequired(true)
.setDescription("Substring of project key (minimum 2 characters)")
.setExampleValue("sonar");
}

@Override
public void handle(Request wsRequest, Response wsResponse) throws Exception {
SuggestionsWsResponse searchWsResponse = doHandle(wsRequest.param(URL_PARAM_QUERY));
writeProtobuf(searchWsResponse, wsRequest, wsResponse);
}

SuggestionsWsResponse doHandle(String query) {
List<Qualifier> resultsPerQualifier = getResultsOfAllQualifiers(query);

return SuggestionsWsResponse.newBuilder()
.addAllResults(resultsPerQualifier)
.build();
}

private List<Qualifier> getResultsOfAllQualifiers(String query) {
return Arrays
.stream(QUALIFIERS)
.flatMap(qualifier -> getResultsOfQualifier(query, qualifier).map(Stream::of).orElseGet(Stream::empty))
.collect(Collectors.toList());
}

private Optional<Qualifier> getResultsOfQualifier(String query, String qualifier) {
ComponentIndexQuery componentIndexQuery = new ComponentIndexQuery(query)
.addQualifier(qualifier)
.setLimit(NUMBER_OF_RESULTS_PER_QUALIFIER);

List<String> uuids = searchInIndex(componentIndexQuery);
if (uuids.isEmpty()) {
return Optional.empty();
}

List<Component> results = fetchFromDatabase(uuids)
.stream()
.map(SuggestionsAction::dtoToComponent)
.collect(Collectors.toList());

Qualifier q = Qualifier.newBuilder()
.setQ(qualifier)
.addAllItems(results)
.build();
return Optional.of(q);
}

private List<String> searchInIndex(ComponentIndexQuery componentIndexQuery) {
return index.search(componentIndexQuery);
}

public List<ComponentDto> fetchFromDatabase(List<String> uuids) {
DbSession dbSession = dbClient.openSession(false);
try {
return dbClient.componentDao().selectByUuids(dbSession, uuids);
} finally {
dbClient.closeSession(dbSession);
}
}

private static Component dtoToComponent(ComponentDto result) {
return Component.newBuilder()
.setKey(result.getKey())
.setName(result.longName())
.build();
}

}

+ 7
- 5
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/IndexComponentsStep.java View File

@@ -20,25 +20,27 @@
package org.sonar.server.computation.task.projectanalysis.step;

import org.sonar.db.component.ResourceIndexDao;
import org.sonar.server.component.index.ComponentIndexer;
import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolder;
import org.sonar.server.computation.task.step.ComputationStep;

/**
* Components are currently indexed in db table RESOURCE_INDEX, not in Elasticsearch
*/
public class IndexComponentsStep implements ComputationStep {

private final ResourceIndexDao resourceIndexDao;
private final ComponentIndexer elasticSearchIndexer;
private final TreeRootHolder treeRootHolder;

public IndexComponentsStep(ResourceIndexDao resourceIndexDao, TreeRootHolder treeRootHolder) {
public IndexComponentsStep(ResourceIndexDao resourceIndexDao, ComponentIndexer elasticSearchIndexer, TreeRootHolder treeRootHolder) {
this.resourceIndexDao = resourceIndexDao;
this.elasticSearchIndexer = elasticSearchIndexer;
this.treeRootHolder = treeRootHolder;
}

@Override
public void execute() {
resourceIndexDao.indexProject(treeRootHolder.getRoot().getUuid());
String projectUuid = treeRootHolder.getRoot().getUuid();
resourceIndexDao.indexProject(projectUuid);
elasticSearchIndexer.index(projectUuid);
}

@Override

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/es/DefaultIndexSettings.java View File

@@ -70,7 +70,7 @@ class DefaultIndexSettings {
.putArray("index.analysis.analyzer.html_analyzer.char_filter", "html_strip")

// Edge NGram filter
.put("index.analysis.filter.gram_filter.type", "edgeNGram")
.put("index.analysis.filter.gram_filter.type", "nGram")
.put("index.analysis.filter.gram_filter.min_gram", 2)
.put("index.analysis.filter.gram_filter.max_gram", 15)
.putArray("index.analysis.filter.gram_filter.token_chars", "letter", "digit", "punctuation", "symbol")

+ 9
- 2
server/sonar-server/src/main/java/org/sonar/server/es/IndexerStartupTask.java View File

@@ -22,6 +22,7 @@ package org.sonar.server.es;
import org.sonar.api.config.Settings;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.server.component.index.ComponentIndexer;
import org.sonar.server.issue.index.IssueIndexer;
import org.sonar.server.measure.index.ProjectMeasuresIndexer;
import org.sonar.server.permission.index.PermissionIndexer;
@@ -39,6 +40,7 @@ public class IndexerStartupTask {
private final UserIndexer userIndexer;
private final ViewIndexer viewIndexer;
private final ProjectMeasuresIndexer projectMeasuresIndexer;
private final ComponentIndexer componentIndexer;
private final Settings settings;

/**
@@ -47,14 +49,16 @@ public class IndexerStartupTask {
* {@link org.sonar.server.issue.index.IssueIndexer}
*/
public IndexerStartupTask(TestIndexer testIndexer, PermissionIndexer permissionIndexer, IssueIndexer issueIndexer,
UserIndexer userIndexer, ViewIndexer viewIndexer, ProjectMeasuresIndexer projectMeasuresIndexer,
Settings settings) {
UserIndexer userIndexer, ViewIndexer viewIndexer, ProjectMeasuresIndexer projectMeasuresIndexer,
ComponentIndexer componentIndexer,
Settings settings) {
this.testIndexer = testIndexer;
this.permissionIndexer = permissionIndexer;
this.issueIndexer = issueIndexer;
this.userIndexer = userIndexer;
this.viewIndexer = viewIndexer;
this.projectMeasuresIndexer = projectMeasuresIndexer;
this.componentIndexer = componentIndexer;
this.settings = settings;
}

@@ -78,6 +82,9 @@ public class IndexerStartupTask {

LOG.info("Index project measures");
projectMeasuresIndexer.index();

LOG.info("Index components");
componentIndexer.index();
}
}


+ 6
- 0
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java View File

@@ -40,6 +40,9 @@ import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.ComponentService;
import org.sonar.server.component.DefaultComponentFinder;
import org.sonar.server.component.DefaultRubyComponentService;
import org.sonar.server.component.index.ComponentIndex;
import org.sonar.server.component.index.ComponentIndexDefinition;
import org.sonar.server.component.index.ComponentIndexer;
import org.sonar.server.component.ws.ComponentsWsModule;
import org.sonar.server.debt.DebtModelBackup;
import org.sonar.server.debt.DebtModelPluginRepository;
@@ -369,6 +372,9 @@ public class PlatformLevel4 extends PlatformLevel {
NewAlerts.class,
NewAlerts.newMetadata(),
ComponentCleanerService.class,
ComponentIndexDefinition.class,
ComponentIndex.class,
ComponentIndexer.class,

FavoriteModule.class,


+ 0
- 5
server/sonar-server/src/main/resources/org/sonar/server/component/ws/components-example-suggestions.json View File

@@ -1,10 +1,7 @@
{
"total": 101,
"results": [
{
"q": "TRK",
"icon": "/images/q/TRK.png",
"name": "Projects",
"items": [
{
"id": 2865,
@@ -34,8 +31,6 @@
},
{
"q": "BRC",
"icon": "/images/q/BRC.png",
"name": "Sub-projects",
"items": [
{
"id": 17221,

+ 5
- 3
server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceTest.java View File

@@ -38,6 +38,7 @@ import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentTesting;
import org.sonar.db.component.ResourceIndexDao;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.server.component.index.ComponentIndexer;
import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.NotFoundException;
@@ -81,15 +82,16 @@ public class ComponentServiceTest {
private DbSession dbSession = dbTester.getSession();
private I18nRule i18n = new I18nRule();
private ProjectMeasuresIndexer projectMeasuresIndexer = new ProjectMeasuresIndexer(system2, dbClient, es.client());
private ComponentIndexer componentIndexer = new ComponentIndexer(dbClient, es.client());
private OrganizationDto organization;

private ComponentService underTest;

@Before
public void setUp() {
i18n.put("qualifier.TRK", "Project");

underTest = new ComponentService(dbClient, i18n, userSession, system2, new ComponentFinder(dbClient), projectMeasuresIndexer);
underTest = new ComponentService(dbClient, i18n, userSession, system2, new ComponentFinder(dbClient), projectMeasuresIndexer, componentIndexer);
organization = dbTester.organizations().insert();
}

@@ -311,7 +313,7 @@ public class ComponentServiceTest {
ComponentTesting.newProjectDto(organizationDto).setId(2L).setKey(projectKey),
ComponentTesting.newProjectDto(organizationDto).setId(3L).setKey(projectKey)));

underTest = new ComponentService(dbClient, i18n, userSession, System2.INSTANCE, new ComponentFinder(dbClient), projectMeasuresIndexer);
underTest = new ComponentService(dbClient, i18n, userSession, System2.INSTANCE, new ComponentFinder(dbClient), projectMeasuresIndexer, componentIndexer);
underTest.create(
session,
newComponentBuilder()

+ 3
- 1
server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java View File

@@ -35,6 +35,7 @@ import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentTesting;
import org.sonar.server.component.index.ComponentIndexer;
import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.ForbiddenException;
@@ -74,8 +75,9 @@ public class ComponentServiceUpdateKeyTest {
private I18nRule i18n = new I18nRule();

private ProjectMeasuresIndexer projectMeasuresIndexer = new ProjectMeasuresIndexer(system2, dbClient, es.client());
private ComponentIndexer componentIndexer = new ComponentIndexer(dbClient, es.client());

private ComponentService underTest = new ComponentService(dbClient, i18n, userSession, system2, new ComponentFinder(dbClient), projectMeasuresIndexer);
private ComponentService underTest = new ComponentService(dbClient, i18n, userSession, system2, new ComponentFinder(dbClient), projectMeasuresIndexer, componentIndexer);

@Before
public void setUp() {

+ 4
- 3
server/sonar-server/src/test/java/org/sonar/server/component/DefaultRubyComponentServiceTest.java View File

@@ -34,14 +34,15 @@ import org.sonar.db.component.ComponentDbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ResourceDao;
import org.sonar.db.component.ResourceDto;
import org.sonar.server.component.index.ComponentIndexer;
import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.favorite.FavoriteUpdater;
import org.sonar.server.i18n.I18nRule;
import org.sonar.server.organization.DefaultOrganizationProvider;
import org.sonar.server.organization.TestDefaultOrganizationProvider;
import org.sonar.server.measure.index.ProjectMeasuresIndexDefinition;
import org.sonar.server.measure.index.ProjectMeasuresIndexer;
import org.sonar.server.organization.DefaultOrganizationProvider;
import org.sonar.server.organization.TestDefaultOrganizationProvider;
import org.sonar.server.permission.PermissionTemplateService;
import org.sonar.server.tester.UserSessionRule;

@@ -73,7 +74,7 @@ public class DefaultRubyComponentServiceTest {

private ResourceDao resourceDao = dbClient.resourceDao();
private ComponentService componentService = new ComponentService(dbClient, i18n, userSession, system2, new ComponentFinder(dbClient),
new ProjectMeasuresIndexer(system2, dbClient, es.client()));
new ProjectMeasuresIndexer(system2, dbClient, es.client()), new ComponentIndexer(dbClient, es.client()));
private PermissionTemplateService permissionTemplateService = mock(PermissionTemplateService.class);
private FavoriteUpdater favoriteUpdater = mock(FavoriteUpdater.class);
private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);

+ 203
- 0
server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexTest.java View File

@@ -0,0 +1,203 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.config.MapSettings;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.utils.System2;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentTesting;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.organization.OrganizationTesting;
import org.sonar.server.es.EsTester;

import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThat;

public class ComponentIndexTest {

private static final String BLA = "bla";
private static final String UUID_DOC = "UUID-DOC-";
private static final String UUID_DOC_1 = UUID_DOC + "1";
private static final String KEY = "KEY-";
private static final String KEY_1 = KEY + "1";

private static final String PREFIX = "Son";
private static final String MIDDLE = "arQ";
private static final String SUFFIX = "ube";
private static final String PREFIX_MIDDLE_SUFFIX = PREFIX + MIDDLE + SUFFIX;

@Rule
public EsTester es = new EsTester(new ComponentIndexDefinition(new MapSettings()));

@Rule
public DbTester db = DbTester.create(System2.INSTANCE);

private ComponentIndex index;
private ComponentIndexer indexer;
private OrganizationDto organization;

@Before
public void setUp() {
index = new ComponentIndex(es.client());
indexer = new ComponentIndexer(db.getDbClient(), es.client());
organization = OrganizationTesting.newOrganizationDto();
}

@Test
public void empty_search() {
assertSearch(emptyList(), BLA, emptyList());
}

@Test
public void exact_match_search() {
assertMatch(BLA, BLA);
}

@Test
public void ignore_case() {
assertMatch("bLa", "BlA");
}

@Test
public void search_for_different_qualifier() {

// create a component of type project
ComponentDto project = ComponentTesting
.newProjectDto(organization, UUID_DOC_1)
.setName(BLA)
.setKey(BLA);

// search for components of type file
ComponentIndexQuery fileQuery = new ComponentIndexQuery(BLA);
fileQuery.addQualifier(Qualifiers.FILE);

// should not have any results
assertThat(search(asList(project), fileQuery)).isEmpty();
}

@Test
public void prefix_match_search() {
assertMatch(PREFIX_MIDDLE_SUFFIX, PREFIX);
}

@Test
public void middle_match_search() {
assertMatch(PREFIX_MIDDLE_SUFFIX, MIDDLE);
}

@Test
public void suffix_match_search() {
assertMatch(PREFIX_MIDDLE_SUFFIX, SUFFIX);
}

@Test
public void exact_match_should_be_shown_first() {
ComponentDto good = newDoc(UUID_DOC + 1, "Current SonarQube Plattform");
ComponentDto better = newDoc(UUID_DOC + 2, "SonarQube Plattform");

assertThat(search(asList(good, better), "SonarQube"))
.containsExactly(better.uuid(), good.uuid());
}

@Test
public void do_not_interpret_input() {
assertNotMatch(BLA, "*");
}

@Test
public void key_match_search() {
assertSearch(
asList(newDoc(UUID_DOC_1, "name is not a match", "matchingKey")),
"matchingKey",
asList(UUID_DOC_1));
}

@Test
public void unmatching_search() {
assertNotMatch(BLA, "blubb");
}

@Test
public void limit_number_of_documents() {
Collection<ComponentDto> docs = IntStream
.rangeClosed(1, 42)
.mapToObj(i -> newDoc(UUID_DOC + i, BLA, KEY + i))
.collect(Collectors.toList());

int pageSize = 41;
ComponentIndexQuery componentIndexQuery = new ComponentIndexQuery(BLA)
.addQualifier(Qualifiers.PROJECT)
.setLimit(pageSize);
assertThat(search(docs, componentIndexQuery)).hasSize(pageSize);
}

private void assertMatch(String name, String query) {
assertSearch(
asList(newDoc(name)),
query,
asList(UUID_DOC_1));
}

private void assertNotMatch(String name, String query) {
assertSearch(
asList(newDoc(name)),
query,
emptyList());
}

private ComponentDto newDoc(String name) {
return newDoc(UUID_DOC_1, name);
}

private ComponentDto newDoc(String uuid, String name) {
return newDoc(uuid, name, KEY_1);
}

private ComponentDto newDoc(String uuid, String name, String key) {
return ComponentTesting
.newProjectDto(organization, uuid)
.setName(name)
.setKey(key);
}

private void assertSearch(Collection<ComponentDto> input, String queryText, Collection<String> expectedOutput) {
assertThat(search(input, queryText))
.hasSameElementsAs(expectedOutput);
}

private List<String> search(Collection<ComponentDto> input, String query) {
return search(input, new ComponentIndexQuery(query).addQualifier(Qualifiers.PROJECT));
}

private List<String> search(Collection<ComponentDto> input, ComponentIndexQuery query) {
input.stream().forEach(indexer::index);
return index.search(query);
}
}

+ 125
- 0
server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexerTest.java View File

@@ -0,0 +1,125 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.Rule;
import org.junit.Test;
import org.sonar.api.config.MapSettings;
import org.sonar.api.utils.System2;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentTesting;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.organization.OrganizationTesting;
import org.sonar.server.es.EsTester;

import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.server.component.index.ComponentIndexDefinition.INDEX_COMPONENTS;
import static org.sonar.server.component.index.ComponentIndexDefinition.TYPE_COMPONENT;

public class ComponentIndexerTest {

private System2 system2 = System2.INSTANCE;

@Rule
public EsTester esTester = new EsTester(new ComponentIndexDefinition(new MapSettings()));

@Rule
public DbTester dbTester = DbTester.create(system2);

private DbClient dbClient = dbTester.getDbClient();
private DbSession dbSession = dbTester.getSession();
private OrganizationDto organization;

@Before
public void setUp() {
organization = OrganizationTesting.newOrganizationDto();
}

@Test
public void index_nothing() {
index();
assertThat(count()).isZero();
}

@Test
public void index_everything() {
insert(ComponentTesting.newProjectDto(organization));

index();
assertThat(count()).isEqualTo(1);
}

@Test
public void index_unexisting_project_while_database_contains_another() {
insert(ComponentTesting.newProjectDto(organization, "UUID-1"));

index("UUID-2");
assertThat(count()).isEqualTo(0);
}

@Test
public void index_one_project() {
ComponentDto project = ComponentTesting.newProjectDto(organization, "UUID-1");
insert(project);

index(project);
assertThat(count()).isEqualTo(1);
}

@Test
public void index_one_project_containing_a_file() {
ComponentDto projectComponent = ComponentTesting.newProjectDto(organization, "UUID-PROJECT-1");
insert(projectComponent);
insert(ComponentTesting.newFileDto(projectComponent));

index(projectComponent);
assertThat(count()).isEqualTo(2);
}

private void insert(ComponentDto component) {
dbClient.componentDao().insert(dbSession, component);
dbSession.commit();
}

private void index() {
createIndexer().index();
}

private void index(ComponentDto component) {
index(component.uuid());
}

private void index(String uuid) {
createIndexer().index(uuid);
}

private long count() {
return esTester.countDocuments(INDEX_COMPONENTS, TYPE_COMPONENT);
}

private ComponentIndexer createIndexer() {
return new ComponentIndexer(dbTester.getDbClient(), esTester.client());
}

}

+ 5
- 2
server/sonar-server/src/test/java/org/sonar/server/component/ws/AppActionTest.java View File

@@ -67,8 +67,11 @@ public class AppActionTest {
@Before
public void setUp() {
insertMetrics();
wsTester = new WsTester(new ComponentsWs(
new AppAction(dbTester.getDbClient(), userSessionRule, new ComponentFinder(dbTester.getDbClient())), mock(SearchViewComponentsAction.class)));
wsTester = new WsTester(
new ComponentsWs(
new AppAction(dbTester.getDbClient(), userSessionRule, new ComponentFinder(dbTester.getDbClient())),
mock(SearchViewComponentsAction.class),
mock(SuggestionsAction.class)));
}

@Test

+ 5
- 2
server/sonar-server/src/test/java/org/sonar/server/component/ws/BulkUpdateKeyActionTest.java View File

@@ -39,6 +39,7 @@ import org.sonar.db.component.ComponentDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.ComponentService;
import org.sonar.server.component.index.ComponentIndexer;
import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.ForbiddenException;
@@ -88,8 +89,10 @@ public class BulkUpdateKeyActionTest {

private ComponentFinder componentFinder = new ComponentFinder(dbClient);

private WsActionTester ws = new WsActionTester(
new BulkUpdateKeyAction(dbClient, componentFinder, new ComponentService(dbClient, null, null, null, null, new ProjectMeasuresIndexer(system2, dbClient, es.client())), userSession));
WsActionTester ws = new WsActionTester(
new BulkUpdateKeyAction(dbClient, componentFinder,
new ComponentService(dbClient, null, null, null, null, new ProjectMeasuresIndexer(system2, dbClient, es.client()), new ComponentIndexer(dbClient, es.client())),
userSession));

@Before
public void setUp() {

+ 1
- 1
server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsModuleTest.java View File

@@ -29,6 +29,6 @@ public class ComponentsWsModuleTest {
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new ComponentsWsModule().configure(container);
assertThat(container.size()).isEqualTo(11 + 2);
assertThat(container.size()).isEqualTo(12 + 2);
}
}

+ 5
- 6
server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsTest.java View File

@@ -26,11 +26,10 @@ 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.RailsHandler;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.Durations;
import org.sonar.db.DbClient;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.index.ComponentIndex;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.WsTester;

@@ -53,8 +52,8 @@ public class ComponentsWsTest {
WsTester tester = new WsTester(new ComponentsWs(
new AppAction(mock(DbClient.class), userSessionRule, mock(ComponentFinder.class)),
new SearchViewComponentsAction(mock(DbClient.class), userSessionRule, mock(ComponentFinder.class)),
new SearchAction(mock(org.sonar.db.DbClient.class), mock(ResourceTypes.class), mock(I18n.class), userSessionRule, languages)
));
new SuggestionsAction(mock(DbClient.class), mock(ComponentIndex.class)),
new SearchAction(mock(DbClient.class), mock(ResourceTypes.class), mock(I18n.class), userSessionRule, languages)));
controller = tester.controller("api/components");
}

@@ -72,9 +71,9 @@ public class ComponentsWsTest {
assertThat(action).isNotNull();
assertThat(action.isInternal()).isTrue();
assertThat(action.isPost()).isFalse();
assertThat(action.handler()).isInstanceOf(RailsHandler.class);
assertThat(action.handler()).isNotNull();
assertThat(action.responseExampleAsString()).isNotEmpty();
assertThat(action.params()).hasSize(2);
assertThat(action.params()).hasSize(1);
}

@Test

+ 6
- 2
server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchViewComponentsActionTest.java View File

@@ -28,13 +28,13 @@ import org.sonar.api.utils.System2;
import org.sonar.api.web.UserRole;
import org.sonar.db.DbTester;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.index.ComponentIndex;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.WsTester;

import static org.mockito.Mockito.mock;


public class SearchViewComponentsActionTest {

@Rule
@@ -50,7 +50,11 @@ public class SearchViewComponentsActionTest {

@Before
public void setUp() {
ws = new WsTester(new ComponentsWs(mock(AppAction.class), new SearchViewComponentsAction(db.getDbClient(), userSessionRule, new ComponentFinder(db.getDbClient()))));
ws = new WsTester(
new ComponentsWs(
mock(AppAction.class),
new SearchViewComponentsAction(db.getDbClient(), userSessionRule, new ComponentFinder(db.getDbClient())),
new SuggestionsAction(db.getDbClient(), mock(ComponentIndex.class))));
}

@Test

+ 90
- 0
server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java View File

@@ -0,0 +1,90 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.ws;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.config.MapSettings;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.utils.System2;
import org.sonar.core.util.stream.Collectors;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.organization.OrganizationTesting;
import org.sonar.server.component.index.ComponentIndex;
import org.sonar.server.component.index.ComponentIndexDefinition;
import org.sonar.server.component.index.ComponentIndexer;
import org.sonar.server.es.EsTester;
import org.sonarqube.ws.WsComponents.SuggestionsWsResponse;

import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.db.component.ComponentTesting.newProjectDto;

public class SuggestionsActionTest {

@Rule
public DbTester db = DbTester.create(System2.INSTANCE);

@Rule
public EsTester es = new EsTester(new ComponentIndexDefinition(new MapSettings()));

private ComponentIndex index;
private ComponentIndexer indexer;
private SuggestionsAction action;
private OrganizationDto organization;

@Before
public void setUp() {
index = new ComponentIndex(es.client());
indexer = new ComponentIndexer(db.getDbClient(), es.client());
action = new SuggestionsAction(db.getDbClient(), index);
organization = OrganizationTesting.newOrganizationDto();
}

@Test
public void exact_match_in_one_qualifier() {
ComponentDto dto = newProjectDto(organization, "project-uuid").setId(42L);
db.getDbClient().componentDao().insert(db.getSession(), dto);
db.commit();

indexer.index();

SuggestionsWsResponse response = action.doHandle(dto.getKey());

// assert match in qualifier "TRK"
assertThat(
response.getResultsList()
.stream()
.map(q -> q.getQ())
.collect(Collectors.toList()))
.containsExactly(Qualifiers.PROJECT);

// assert correct id to be found
assertThat(
response.getResultsList()
.stream()
.flatMap(q -> q.getItemsList().stream())
.map(c -> c.getKey())
.collect(Collectors.toList()))
.containsExactly(dto.getKey());
}
}

+ 4
- 2
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/IndexComponentsStepTest.java View File

@@ -22,10 +22,11 @@ package org.sonar.server.computation.task.projectanalysis.step;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.db.component.ResourceIndexDao;
import org.sonar.server.component.index.ComponentIndexer;
import org.sonar.server.computation.task.projectanalysis.batch.BatchReportReaderRule;
import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolderRule;
import org.sonar.server.computation.task.projectanalysis.component.Component;
import org.sonar.server.computation.task.projectanalysis.component.ReportComponent;
import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolderRule;
import org.sonar.server.computation.task.projectanalysis.component.ViewsComponent;
import org.sonar.server.computation.task.step.ComputationStep;

@@ -45,7 +46,8 @@ public class IndexComponentsStepTest extends BaseStepTest {
public BatchReportReaderRule reportReader = new BatchReportReaderRule();

ResourceIndexDao resourceIndexDao = mock(ResourceIndexDao.class);
IndexComponentsStep underTest = new IndexComponentsStep(resourceIndexDao, treeRootHolder);
ComponentIndexer elasticSearchIndexer = mock(ComponentIndexer.class);
IndexComponentsStep underTest = new IndexComponentsStep(resourceIndexDao, elasticSearchIndexer, treeRootHolder);

@Test
public void call_indexProject_of_dao_for_project() {

+ 3
- 1
server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/AddProjectActionTest.java View File

@@ -35,6 +35,7 @@ import org.sonar.db.qualityprofile.QualityProfileDbTester;
import org.sonar.db.qualityprofile.QualityProfileDto;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.ComponentService;
import org.sonar.server.component.index.ComponentIndexer;
import org.sonar.server.es.EsTester;
import org.sonar.server.language.LanguageTesting;
import org.sonar.server.measure.index.ProjectMeasuresIndexDefinition;
@@ -79,7 +80,8 @@ public class AddProjectActionTest {

private WsActionTester ws = new WsActionTester(new AddProjectAction(projectAssociationParameters,
qProfileProjectOperations, new ProjectAssociationFinder(new QProfileLookup(dbClient),
new ComponentService(dbClient, null, userSession, null, new ComponentFinder(dbClient), new ProjectMeasuresIndexer(system2, dbClient, es.client()))),
new ComponentService(dbClient, null, userSession, null, new ComponentFinder(dbClient), new ProjectMeasuresIndexer(system2, dbClient, es.client()),
new ComponentIndexer(dbClient, es.client()))),
userSession));

@Before

+ 0
- 100
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/components_controller.rb View File

@@ -1,100 +0,0 @@
#
# SonarQube, open source software quality management tool.
# Copyright (C) 2008-2016 SonarSource
# mailto:contact AT sonarsource DOT com
#
# SonarQube 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.
#
# SonarQube 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.
#
class Api::ComponentsController < Api::ApiController

# Do not exceed 1000 because of the Oracle limitation on IN statements
MAX_RESULTS = 6

# Internal WS for the top-right search engine
def suggestions
search = params[:s]
bad_request("Minimum search is #{ResourceIndex::MIN_SEARCH_SIZE} characters") if search.empty? || search.to_s.size<ResourceIndex::MIN_SEARCH_SIZE

# SONAR-5198 Escape '_' on Oracle and MsSQL
dialect = java_facade.getDatabase().getDialect().getId()
additional_escape = dialect == 'oracle' || dialect == 'mssql' ? "ESCAPE '\\'" : ''

key = escape_like(search).downcase
results = ResourceIndex.all(:select => 'distinct(component_uuid),root_component_uuid,qualifier,name_size', # optimization to not load unused columns like 'kee'
:conditions => ['kee like ? ' + additional_escape, key + '%'],
:order => 'name_size')

results = select_authorized(:user, results)

resource_uuids=[]
resource_indexes_by_qualifier={}
results.each do |resource_index|
qualifier = fix_qualifier(resource_index.qualifier)
resource_indexes_by_qualifier[qualifier] ||= []
array = resource_indexes_by_qualifier[qualifier]
if array.size < MAX_RESULTS
resource_uuids << resource_index.component_uuid
array << resource_index
end
end

resources_by_uuid = {}
unless resource_uuids.empty?
Project.find(:all, :conditions => ['uuid in (?)', resource_uuids]).each do |resource|
resources_by_uuid[resource.uuid]=resource
end
end

json = {'total' => results.size}
json_results = []
java_facade.getResourceTypes().each do |resource_type|
qualifier_results={}
qualifier=resource_type.getQualifier()
qualifier_results['q']=qualifier
qualifier_results['icon']=resource_type.getIconPath()
qualifier_results['name']=Api::Utils.message("qualifiers.#{qualifier}")
resource_indexes=resource_indexes_by_qualifier[qualifier]||[]
qualifier_results['items']=resource_indexes.map do |resource_index|
resource=resources_by_uuid[resource_index.component_uuid]
{
'key' => resource.key,
'name' => resource.name(true)
}
end
json_results<<qualifier_results
end
json['results']=json_results

respond_to do |format|
format.json { render :json => jsonp(json) }
format.xml { render :xml => xml_not_supported }
format.text { render :text => text_not_supported }
end
end

private

def fix_qualifier(q)
case q
when 'CLA' then
'FIL'
when 'PAC' then
'DIR'
else
q
end
end

end

+ 8
- 0
sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java View File

@@ -30,6 +30,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.resources.Scopes;
@@ -240,6 +241,13 @@ public class ComponentDao implements Dao {
return mapper(session).countGhostProjects(parameters);
}

/**
* Selects all enabled components. The result is not returned (since it is usually too big), but handed over to the <code>handler</code>
*/
public void selectAll(DbSession session, ResultHandler handler) {
mapper(session).selectAll(handler);
}

/**
* Retrieves all components with a specific root project Uuid, no other filtering is done by this method.
*

+ 3
- 0
sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java View File

@@ -25,6 +25,7 @@ import java.util.Map;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

/**
@@ -131,6 +132,8 @@ public interface ComponentMapper {

long countGhostProjects(Map<String, Object> parameters);

void selectAll(ResultHandler handler);

void insert(ComponentDto componentDto);

void insertBatch(ComponentDto componentDto);

+ 7
- 0
sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml View File

@@ -486,6 +486,13 @@
</if>
</sql>

<select id="selectAll" parameterType="map" resultType="Component">
select distinct
<include refid="componentColumns"/>
from projects p
where p.enabled=${_true}
</select>

<sql id="insertSql">
INSERT INTO projects (
organization_uuid,

+ 1
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java View File

@@ -30,6 +30,7 @@ public class ComponentsWsParameters {
public static final String ACTION_SHOW = "show";
public static final String ACTION_BULK_UPDATE_KEY = "bulk_update_key";
public static final String ACTION_SEARCH_PROJECTS = "search_projects";
public static final String ACTION_SUGGESTIONS = "suggestions";

// parameters
public static final String PARAM_QUALIFIERS = "qualifiers";

+ 10
- 0
sonar-ws/src/main/protobuf/ws-components.proto View File

@@ -46,6 +46,16 @@ message ShowWsResponse {
repeated Component ancestors = 3;
}

// WS api/components/suggestions
message SuggestionsWsResponse {
repeated Qualifier results = 2;
message Qualifier {
optional string q = 1;
repeated Component items = 4;
}
}

// WS api/components/prepare_bulk_update_key
message BulkUpdateKeyWsResponse {
repeated Key keys = 1;

Loading…
Cancel
Save