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
@@ -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, |
@@ -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 |
@@ -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 |
@@ -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; | |||
} | |||
} |
@@ -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)); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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; |
@@ -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); | |||
} | |||
} |
@@ -30,6 +30,7 @@ public class ComponentsWsModule extends Module { | |||
// actions | |||
AppAction.class, | |||
SearchAction.class, | |||
SuggestionsAction.class, | |||
TreeAction.class, | |||
ShowAction.class, | |||
SearchViewComponentsAction.class, |
@@ -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(); | |||
} | |||
} |
@@ -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 |
@@ -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") |
@@ -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(); | |||
} | |||
} | |||
@@ -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, | |||
@@ -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, |
@@ -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() |
@@ -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() { |
@@ -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); |
@@ -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); | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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 |
@@ -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() { |
@@ -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); | |||
} | |||
} |
@@ -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 |
@@ -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 |
@@ -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()); | |||
} | |||
} |
@@ -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() { |
@@ -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 |
@@ -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 |
@@ -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. | |||
* |
@@ -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); |
@@ -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, |
@@ -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"; |
@@ -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; |