@@ -48,8 +48,10 @@ import org.sonar.api.utils.System2; | |||
import org.sonar.core.util.UuidFactoryFast; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.Pagination; | |||
import org.sonar.db.RowNotFoundException; | |||
import org.sonar.db.issue.ImpactDto; | |||
import org.sonar.db.qualityprofile.QProfileDto; | |||
import org.sonar.db.rule.RuleDto.Scope; | |||
import static com.google.common.collect.Sets.newHashSet; | |||
@@ -63,10 +65,11 @@ import static org.assertj.core.api.Assertions.tuple; | |||
import static org.sonar.api.issue.impact.SoftwareQuality.MAINTAINABILITY; | |||
import static org.sonar.api.issue.impact.SoftwareQuality.RELIABILITY; | |||
import static org.sonar.api.issue.impact.SoftwareQuality.SECURITY; | |||
import static org.sonar.api.rule.RuleStatus.*; | |||
import static org.sonar.api.rule.RuleStatus.DEPRECATED; | |||
import static org.sonar.api.rule.RuleStatus.READY; | |||
import static org.sonar.api.rule.RuleStatus.REMOVED; | |||
import static org.sonar.db.Pagination.forPage; | |||
import static org.sonar.db.rule.RuleListQuery.RuleListQueryBuilder.newRuleListQueryBuilder; | |||
public class RuleDaoIT { | |||
private static final String UNKNOWN_RULE_UUID = "unknown-uuid"; | |||
@@ -1224,6 +1227,69 @@ public class RuleDaoIT { | |||
.isInstanceOf(PersistenceException.class); | |||
} | |||
@Test | |||
public void selectRules_whenPagination_shouldReturnSpecificPage() { | |||
db.rules().insert(rule -> rule.setUuid("uuid_1")); | |||
db.rules().insert(rule -> rule.setUuid("uuid_2")); | |||
db.rules().insert(rule -> rule.setUuid("uuid_3")); | |||
db.rules().insert(rule -> rule.setUuid("uuid_4")); | |||
Pagination pagination = Pagination.forPage(1).andSize(2); | |||
RuleListResult ruleListResult = underTest.selectRules(db.getSession(), newRuleListQueryBuilder().build(), pagination); | |||
assertThat(ruleListResult.getTotal()).isEqualTo(4); | |||
assertThat(ruleListResult.getUuids()).containsExactly("uuid_1", "uuid_2"); | |||
} | |||
@Test | |||
public void selectRules_whenQueriedWithCreatedAt_shouldReturnRulesCreatedAfterCorrectlySorted() { | |||
long baseTime = 1696028921248L; | |||
db.rules().insert(rule -> rule.setUuid("uuid_1").setCreatedAt(baseTime)); | |||
db.rules().insert(rule -> rule.setUuid("uuid_2").setCreatedAt(baseTime + 1000)); | |||
db.rules().insert(rule -> rule.setUuid("uuid_3").setCreatedAt(baseTime - 2000)); | |||
db.rules().insert(rule -> rule.setUuid("uuid_4").setCreatedAt(baseTime - 1)); | |||
db.rules().insert(rule -> rule.setUuid("uuid_5").setCreatedAt(baseTime + 1)); | |||
RuleListQuery ruleListQuery = newRuleListQueryBuilder().createdAt(baseTime).sortField("createdAt").sortDirection("desc").build(); | |||
RuleListResult ruleListResult = underTest.selectRules(db.getSession(), ruleListQuery, Pagination.all()); | |||
assertThat(ruleListResult.getUuids()).containsExactly("uuid_2", "uuid_5", "uuid_1"); | |||
assertThat(ruleListResult.getTotal()).isEqualTo(3); | |||
} | |||
@Test | |||
public void selectRules_whenQueriedWithLanguage_shouldReturnOnlyRulesWithLanguage() { | |||
String queriedLang = "java"; | |||
db.rules().insert(rule -> rule.setUuid("uuid_1").setLanguage(queriedLang)); | |||
db.rules().insert(rule -> rule.setUuid("uuid_2").setLanguage("cpp")); | |||
db.rules().insert(rule -> rule.setUuid("uuid_3").setLanguage("js")); | |||
db.rules().insert(rule -> rule.setUuid("uuid_4").setLanguage(queriedLang)); | |||
RuleListQuery ruleListQuery = newRuleListQueryBuilder().language(queriedLang).build(); | |||
RuleListResult ruleListResult = underTest.selectRules(db.getSession(), ruleListQuery, Pagination.all()); | |||
assertThat(ruleListResult.getUuids()).containsExactly("uuid_1", "uuid_4"); | |||
assertThat(ruleListResult.getTotal()).isEqualTo(2); | |||
} | |||
@Test | |||
public void selectRules_whenQueriedWithQualityProfile_shouldReturnOnlyRulesActiveForThisQualityProfile() { | |||
QProfileDto profile = db.qualityProfiles().insert(); | |||
RuleDto rule1 = db.rules().insert(rule -> rule.setUuid("uuid_1")); | |||
db.rules().insert(rule -> rule.setUuid("uuid_2")); | |||
RuleDto rule3 = db.rules().insert(rule -> rule.setUuid("uuid_3")); | |||
db.rules().insert(rule -> rule.setUuid("uuid_4")); | |||
db.qualityProfiles().activateRule(profile, rule1); | |||
db.qualityProfiles().activateRule(profile, rule3); | |||
RuleListQuery ruleListQuery = newRuleListQueryBuilder().profileUuid(profile.getRulesProfileUuid()).build(); | |||
RuleListResult ruleListResult = underTest.selectRules(db.getSession(), ruleListQuery, Pagination.all()); | |||
assertThat(ruleListResult.getUuids()).containsExactly("uuid_1", "uuid_3"); | |||
assertThat(ruleListResult.getTotal()).isEqualTo(2); | |||
} | |||
private static ImpactDto newRuleDefaultImpact(SoftwareQuality softwareQuality, org.sonar.api.issue.impact.Severity severity) { | |||
return new ImpactDto() | |||
.setUuid(UuidFactoryFast.getInstance().create()) |
@@ -270,4 +270,11 @@ public class RuleDao implements Dao { | |||
private static String toLowerCaseAndSurroundWithPercentSigns(@Nullable String query) { | |||
return isBlank(query) ? PERCENT_SIGN : (PERCENT_SIGN + query.toLowerCase(Locale.ENGLISH) + PERCENT_SIGN); | |||
} | |||
public RuleListResult selectRules(DbSession dbSession, RuleListQuery ruleListQuery, Pagination pagination) { | |||
return new RuleListResult( | |||
mapper(dbSession).selectRules(ruleListQuery, pagination), | |||
mapper(dbSession).countByQuery(ruleListQuery)); | |||
} | |||
} |
@@ -0,0 +1,102 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.db.rule; | |||
import javax.annotation.Nullable; | |||
public class RuleListQuery { | |||
private final Long createdAt; | |||
private final String language; | |||
private final String profileUuid; | |||
private final String sortField; | |||
private final String sortDirection; | |||
private RuleListQuery(Long createdAt, String language, String profileUuid, String sortField, String sortDirection) { | |||
this.createdAt = createdAt; | |||
this.language = language; | |||
this.profileUuid = profileUuid; | |||
this.sortField = sortField; | |||
this.sortDirection = sortDirection; | |||
} | |||
public Long getCreatedAt() { | |||
return createdAt; | |||
} | |||
public String getLanguage() { | |||
return language; | |||
} | |||
public String getProfileUuid() { | |||
return profileUuid; | |||
} | |||
public String getSortField() { | |||
return sortField; | |||
} | |||
public String getSortDirection() { | |||
return sortDirection; | |||
} | |||
public static final class RuleListQueryBuilder { | |||
private Long createdAt; | |||
private String language; | |||
private String profileUuid; | |||
private String sortField; | |||
private String sortDirection = "asc"; | |||
private RuleListQueryBuilder() { | |||
} | |||
public static RuleListQueryBuilder newRuleListQueryBuilder() { | |||
return new RuleListQueryBuilder(); | |||
} | |||
public RuleListQueryBuilder createdAt(@Nullable Long createdAt) { | |||
this.createdAt = createdAt; | |||
return this; | |||
} | |||
public RuleListQueryBuilder language(String language) { | |||
this.language = language; | |||
return this; | |||
} | |||
public RuleListQueryBuilder profileUuid(@Nullable String profileUuid) { | |||
this.profileUuid = profileUuid; | |||
return this; | |||
} | |||
public RuleListQueryBuilder sortField(String sortField) { | |||
this.sortField = sortField; | |||
return this; | |||
} | |||
public RuleListQueryBuilder sortDirection(String sortDirection) { | |||
this.sortDirection = sortDirection; | |||
return this; | |||
} | |||
public RuleListQuery build() { | |||
return new RuleListQuery(createdAt, language, profileUuid, sortField, sortDirection); | |||
} | |||
} | |||
} |
@@ -0,0 +1,40 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.db.rule; | |||
import java.util.List; | |||
public class RuleListResult { | |||
private final List<String> uuids; | |||
private final long total; | |||
public RuleListResult(List<String> uuids, long total) { | |||
this.uuids = uuids; | |||
this.total = total; | |||
} | |||
public List<String> getUuids() { | |||
return uuids; | |||
} | |||
public long getTotal() { | |||
return total; | |||
} | |||
} |
@@ -91,4 +91,8 @@ public interface RuleMapper { | |||
void deleteRuleTags(String ruleUuid); | |||
List<String> selectTags(@Param("query") String query, @Param("pagination") Pagination pagination); | |||
List<String> selectRules(@Param("query") RuleListQuery query, @Param("pagination") Pagination pagination); | |||
Long countByQuery(@Param("query") RuleListQuery query); | |||
} |
@@ -619,5 +619,42 @@ | |||
<include refid="org.sonar.db.common.Common.pagination"/> | |||
</select> | |||
<select id="selectRules"> | |||
select r.uuid from rules r | |||
<include refid="queryList"/> | |||
<choose> | |||
<when test="query.sortField == 'createdAt'"> | |||
order by r.created_at ${query.sortDirection} | |||
</when> | |||
<otherwise> | |||
order by r.uuid | |||
</otherwise> | |||
</choose> | |||
<include refid="org.sonar.db.common.Common.pagination"/> | |||
</select> | |||
<select id="countByQuery" resultType="long"> | |||
select count(r.uuid) from rules r | |||
<include refid="queryList"/> | |||
</select> | |||
<sql id="queryList"> | |||
<if test="query.profileUuid != null"> | |||
inner join active_rules ar on ar.rule_uuid = r.uuid | |||
</if> | |||
where | |||
r.status != 'REMOVED' | |||
and r.is_external = ${_false} | |||
<if test="query.createdAt != null"> | |||
and r.created_at >= #{query.createdAt,jdbcType=BIGINT} | |||
</if> | |||
<if test="query.language != null"> | |||
and r.language = #{query.language,jdbcType=VARCHAR} | |||
</if> | |||
<if test="query.profileUuid != null"> | |||
and ar.profile_uuid = #{query.profileUuid,jdbcType=VARCHAR} | |||
</if> | |||
</sql> | |||
</mapper> | |||
@@ -72,7 +72,7 @@ import { | |||
searchUsers, | |||
setDefaultProfile, | |||
} from '../quality-profiles'; | |||
import { getRuleDetails, searchRules } from '../rules'; | |||
import { getRuleDetails, searchRules, listRules } from '../rules'; | |||
jest.mock('../../api/rules'); | |||
@@ -110,6 +110,7 @@ export default class QualityProfilesServiceMock { | |||
jest.mocked(copyProfile).mockImplementation(this.handleCopyProfile); | |||
jest.mocked(getImporters).mockImplementation(this.handleGetImporters); | |||
jest.mocked(searchRules).mockImplementation(this.handleSearchRules); | |||
jest.mocked(listRules).mockImplementation(this.handleListRules); | |||
jest.mocked(compareProfiles).mockImplementation(this.handleCompareQualityProfiles); | |||
jest.mocked(activateRule).mockImplementation(this.handleActivateRule); | |||
jest.mocked(deactivateRule).mockImplementation(this.handleDeactivateRule); | |||
@@ -522,6 +523,11 @@ export default class QualityProfilesServiceMock { | |||
return this.reply(this.searchRulesResponse); | |||
}; | |||
handleListRules = (data: SearchRulesQuery): Promise<SearchRulesResponse> => { | |||
// Both APIs are mocked the same way, this method is only here to make it explicit. | |||
return this.handleSearchRules(data); | |||
}; | |||
handleGetQualityProfile = () => { | |||
return this.reply({ | |||
profile: mockQualityProfile(), |
@@ -43,6 +43,10 @@ export function searchRules(data: SearchRulesQuery): Promise<SearchRulesResponse | |||
return getJSON('/api/rules/search', data).catch(throwGlobalError); | |||
} | |||
export function listRules(data: SearchRulesQuery): Promise<SearchRulesResponse> { | |||
return getJSON('/api/rules/list', data).catch(throwGlobalError); | |||
} | |||
export function getRuleRepositories(parameters: { | |||
q: string; | |||
}): Promise<Array<{ key: string; language: string; name: string }>> { |
@@ -21,7 +21,7 @@ import { DiscreetLink, Link, Note } from 'design-system'; | |||
import { noop, sortBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import { useIntl } from 'react-intl'; | |||
import { searchRules } from '../../../api/rules'; | |||
import { listRules } from '../../../api/rules'; | |||
import { toShortISO8601String } from '../../../helpers/dates'; | |||
import { translateWithParameters } from '../../../helpers/l10n'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
@@ -54,7 +54,7 @@ export default function EvolutionRules() { | |||
s: 'createdAt', | |||
}; | |||
searchRules(data).then(({ actives, rules, paging: { total } }) => { | |||
listRules(data).then(({ actives, rules, paging: { total } }) => { | |||
setLatestRules(sortBy(parseRules(rules, actives), 'langName')); | |||
setLatestRulesTotal(total); | |||
}, noop); |
@@ -1,51 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.rule.ws; | |||
import java.util.List; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.api.resources.Languages; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.qualityprofile.ActiveRuleDto; | |||
import org.sonar.db.qualityprofile.QProfileDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonarqube.ws.Rules; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
public class ActiveRuleCompleterIT { | |||
@Rule | |||
public DbTester dbTester = DbTester.create(); | |||
@Test | |||
public void test_completeShow() { | |||
ActiveRuleCompleter underTest = new ActiveRuleCompleter(dbTester.getDbClient(), new Languages()); | |||
RuleDto rule = dbTester.rules().insert(); | |||
QProfileDto qualityProfile = dbTester.qualityProfiles().insert(); | |||
ActiveRuleDto activeRule = dbTester.qualityProfiles().activateRule(qualityProfile, rule); | |||
List<Rules.Active> result = underTest.completeShow(dbTester.getSession(), rule); | |||
assertThat(result).extracting(Rules.Active::getQProfile).containsExactlyInAnyOrder(qualityProfile.getKee()); | |||
assertThat(result).extracting(Rules.Active::getSeverity).containsExactlyInAnyOrder(activeRule.getSeverityString()); | |||
} | |||
} |
@@ -19,61 +19,130 @@ | |||
*/ | |||
package org.sonar.server.rule.ws; | |||
import java.text.ParseException; | |||
import java.text.SimpleDateFormat; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.api.resources.Languages; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.qualityprofile.QProfileDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonar.db.rule.RuleTesting; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.language.LanguageTesting; | |||
import org.sonar.server.rule.RuleDescriptionFormatter; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.text.MacroInterpreter; | |||
import org.sonar.server.ws.TestRequest; | |||
import org.sonar.server.ws.WsActionTester; | |||
import org.sonarqube.ws.Rules; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.mockito.Mockito.mock; | |||
public class ListActionIT { | |||
private static final String RULE_KEY_1 = "S001"; | |||
private static final String RULE_KEY_2 = "S002"; | |||
private static final String RULE_KEY_1 = "java:S001"; | |||
private static final String RULE_KEY_2 = "java:S002"; | |||
@Rule | |||
public DbTester dbTester = DbTester.create(System2.INSTANCE); | |||
public DbTester db = DbTester.create(System2.INSTANCE); | |||
ListAction underTest = new ListAction(dbTester.getDbClient()); | |||
@org.junit.Rule | |||
public UserSessionRule userSession = UserSessionRule.standalone(); | |||
WsActionTester tester = new WsActionTester(underTest); | |||
private final Languages languages = LanguageTesting.newLanguages("java", "js"); | |||
private final MacroInterpreter macroInterpreter = mock(MacroInterpreter.class); | |||
private final RuleMapper ruleMapper = new RuleMapper(languages, macroInterpreter, new RuleDescriptionFormatter()); | |||
private final RuleWsSupport ruleWsSupport = new RuleWsSupport(db.getDbClient(), userSession); | |||
private final RulesResponseFormatter rulesResponseFormatter = new RulesResponseFormatter(db.getDbClient(), ruleWsSupport, ruleMapper, languages); | |||
private final WsActionTester ws = new WsActionTester(new ListAction(db.getDbClient(), rulesResponseFormatter)); | |||
@Test | |||
public void define() { | |||
WebService.Action def = tester.getDef(); | |||
assertThat(def.params()).isEmpty(); | |||
public void define_shouldDefineParameters() { | |||
WebService.Action def = ws.getDef(); | |||
assertThat(def.params()).extracting(WebService.Param::key) | |||
.containsExactlyInAnyOrder("asc", "p", "s", "ps", "available_since", "qprofile"); | |||
} | |||
@Test | |||
public void return_rules_in_protobuf() { | |||
dbTester.rules().insert(RuleTesting.newRule(RuleKey.of("java", RULE_KEY_1)).setConfigKey(null).setName(null)); | |||
dbTester.rules().insert(RuleTesting.newRule(RuleKey.of("java", RULE_KEY_2)).setConfigKey("I002").setName("Rule Two")); | |||
dbTester.getSession().commit(); | |||
public void execute_shouldReturnRules() { | |||
db.rules().insert(RuleTesting.newRule(RuleKey.parse(RULE_KEY_1)).setConfigKey(null).setName(null)); | |||
db.rules().insert(RuleTesting.newRule(RuleKey.parse(RULE_KEY_2)).setConfigKey("I002").setName("Rule Two")); | |||
db.getSession().commit(); | |||
Rules.ListResponse listResponse = tester.newRequest() | |||
Rules.ListResponse listResponse = ws.newRequest() | |||
.executeProtobuf(Rules.ListResponse.class); | |||
assertThat(listResponse.getRulesCount()).isEqualTo(2); | |||
Rules.ListResponse.Rule ruleS001 = getRule(listResponse, RULE_KEY_1); | |||
Rules.Rule ruleS001 = getRule(listResponse, RULE_KEY_1); | |||
assertThat(ruleS001.getKey()).isEqualTo(RULE_KEY_1); | |||
assertThat(ruleS001.getInternalKey()).isEmpty(); | |||
assertThat(ruleS001.getName()).isEmpty(); | |||
Rules.ListResponse.Rule ruleS002 = getRule(listResponse, RULE_KEY_2); | |||
Rules.Rule ruleS002 = getRule(listResponse, RULE_KEY_2); | |||
assertThat(ruleS002.getKey()).isEqualTo(RULE_KEY_2); | |||
assertThat(ruleS002.getInternalKey()).isEqualTo("I002"); | |||
assertThat(ruleS002.getName()).isEqualTo("Rule Two"); | |||
} | |||
private Rules.ListResponse.Rule getRule(Rules.ListResponse listResponse, String ruleKey) { | |||
Optional<Rules.ListResponse.Rule> rule = listResponse.getRulesList().stream() | |||
@Test | |||
public void execute_shouldReturnFilteredRules_whenQProfileDefined() { | |||
QProfileDto profile = db.qualityProfiles().insert(p -> p.setLanguage("java")); | |||
RuleDto rule = db.rules().insert(r -> r.setRuleKey(RuleKey.parse(RULE_KEY_1))); | |||
db.qualityProfiles().activateRule(profile, rule); | |||
Rules.ListResponse result = ws.newRequest() | |||
.setParam("qprofile", profile.getKee()) | |||
.executeProtobuf(Rules.ListResponse.class); | |||
assertThat(result.getPaging().getTotal()).isOne(); | |||
assertThat(result.getPaging().getPageIndex()).isOne(); | |||
assertThat(result.getRulesCount()).isOne(); | |||
assertThat(result.getActives()).isNotNull(); | |||
Map<String, Rules.ActiveList> activeRules = result.getActives().getActivesMap(); | |||
assertThat(activeRules.get(rule.getKey().toString())).isNotNull(); | |||
assertThat(activeRules.get(rule.getKey().toString()).getActiveListList()).hasSize(1); | |||
} | |||
@Test | |||
public void execute_shouldReturnFilteredRules_whenAvailableSinceDefined() throws ParseException { | |||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); | |||
long recentDay = dateFormat.parse("2023-02-20").getTime(); | |||
long oldDay = dateFormat.parse("2022-09-17").getTime(); | |||
db.rules().insert(r -> r.setRuleKey(RuleKey.parse(RULE_KEY_1)).setCreatedAt(recentDay)); | |||
db.rules().insert(r -> r.setRuleKey(RuleKey.parse(RULE_KEY_2)).setCreatedAt(oldDay)); | |||
Rules.ListResponse result = ws.newRequest() | |||
.setParam("available_since", "2022-11-23") | |||
.executeProtobuf(Rules.ListResponse.class); | |||
assertThat(result.getRulesCount()).isOne(); | |||
assertThat(result.getRulesList().stream().map(Rules.Rule::getKey)).containsOnly(RULE_KEY_1); | |||
} | |||
@Test | |||
public void execute_shouldFailWithNotFoundException_whenQProfileDoesNotExist() { | |||
String unknownProfile = "unknown_profile"; | |||
TestRequest request = ws.newRequest() | |||
.setParam("qprofile", unknownProfile); | |||
assertThatThrownBy(() -> request.executeProtobuf(Rules.SearchResponse.class)) | |||
.isInstanceOf(NotFoundException.class) | |||
.hasMessage("The specified qualityProfile '" + unknownProfile + "' does not exist"); | |||
} | |||
private Rules.Rule getRule(Rules.ListResponse listResponse, String ruleKey) { | |||
Optional<Rules.Rule> rule = listResponse.getRulesList().stream() | |||
.filter(r -> ruleKey.equals(r.getKey())) | |||
.findFirst(); | |||
assertThat(rule).isPresent(); |
@@ -130,13 +130,12 @@ public class SearchActionIT { | |||
private final RuleIndexer ruleIndexer = new RuleIndexer(es.client(), db.getDbClient()); | |||
private final ActiveRuleIndexer activeRuleIndexer = new ActiveRuleIndexer(db.getDbClient(), es.client()); | |||
private final Languages languages = LanguageTesting.newLanguages(JAVA, "js"); | |||
private final ActiveRuleCompleter activeRuleCompleter = new ActiveRuleCompleter(db.getDbClient(), languages); | |||
private final RuleQueryFactory ruleQueryFactory = new RuleQueryFactory(db.getDbClient()); | |||
private final MacroInterpreter macroInterpreter = mock(MacroInterpreter.class); | |||
private final QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class); | |||
private final RuleMapper ruleMapper = new RuleMapper(languages, macroInterpreter, new RuleDescriptionFormatter()); | |||
private final SearchAction underTest = new SearchAction(ruleIndex, activeRuleCompleter, ruleQueryFactory, db.getDbClient(), ruleMapper, | |||
new RuleWsSupport(db.getDbClient(), userSession)); | |||
private final SearchAction underTest = new SearchAction(ruleIndex, ruleQueryFactory, db.getDbClient(), | |||
new RulesResponseFormatter(db.getDbClient(), new RuleWsSupport(db.getDbClient(), userSession), ruleMapper, languages)); | |||
private final TypeValidations typeValidations = new TypeValidations(asList(new StringTypeValidation(), new IntegerTypeValidation())); | |||
private final SonarQubeVersion sonarQubeVersion = new SonarQubeVersion(Version.create(10, 3)); | |||
private final RuleActivator ruleActivator = new RuleActivator(System2.INSTANCE, db.getDbClient(), typeValidations, userSession, | |||
@@ -528,9 +527,9 @@ public class SearchActionIT { | |||
assertThat(result.getFacets().getFacets(0).getValuesList()) | |||
.extracting(v -> entry(v.getVal(), v.getCount())).contains( | |||
entry(CleanCodeAttribute.COMPLETE.getAttributeCategory().name(), 1L), | |||
entry(CleanCodeAttribute.CONVENTIONAL.getAttributeCategory().name(), 0L) | |||
); | |||
entry(CleanCodeAttribute.CONVENTIONAL.getAttributeCategory().name(), 0L)); | |||
} | |||
@Test | |||
public void should_included_selected_non_matching_tag_in_facet() { | |||
RuleDto rule = db.rules().insert(setSystemTags("tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", "tag8", "tag9", "tagA")); |
@@ -87,10 +87,10 @@ public class ShowActionIT { | |||
private final UuidFactory uuidFactory = UuidFactoryFast.getInstance(); | |||
private final MacroInterpreter macroInterpreter = mock(MacroInterpreter.class); | |||
private final Languages languages = new Languages(newLanguage("xoo", "Xoo")); | |||
private final RuleWsSupport ruleWsSupport = new RuleWsSupport(db.getDbClient(), userSession); | |||
private final RuleMapper ruleMapper = new RuleMapper(languages, macroInterpreter, new RuleDescriptionFormatter()); | |||
private final WsActionTester ws = new WsActionTester( | |||
new ShowAction(db.getDbClient(), new RuleMapper(languages, macroInterpreter, new RuleDescriptionFormatter()), | |||
new ActiveRuleCompleter(db.getDbClient(), languages), | |||
new RuleWsSupport(db.getDbClient(), userSession))); | |||
new ShowAction(db.getDbClient(), new RulesResponseFormatter(db.getDbClient(), ruleWsSupport, ruleMapper, languages))); | |||
private UserDto userDto; | |||
@Before | |||
@@ -150,7 +150,6 @@ public class ShowActionIT { | |||
.containsExactly(rule.getTags().toArray(new String[0])); | |||
} | |||
//<test case name>_when<conditionInCamelCase>_should<assertionInCamelCase> | |||
@Test | |||
public void returnRuleCleanCodeFields_whenEndpointIsCalled() { | |||
RuleDto rule = db.rules().insert(setTags("tag1", "tag2"), r -> r.setNoteData(null).setNoteUserUuid(null)); |
@@ -187,7 +187,7 @@ public class CreateAction implements RulesWsAction { | |||
templateRule.ifPresent(templateRules::add); | |||
} | |||
List<RuleParamDto> ruleParameters = dbClient.ruleDao().selectRuleParamsByRuleUuids(dbSession, singletonList(rule.getUuid())); | |||
SearchAction.SearchResult searchResult = new SearchAction.SearchResult() | |||
RulesResponseFormatter.SearchResult searchResult = new RulesResponseFormatter.SearchResult() | |||
.setRuleParameters(ruleParameters) | |||
.setTemplateRules(templateRules) | |||
.setTotal(1L); |
@@ -19,61 +19,209 @@ | |||
*/ | |||
package org.sonar.server.rule.ws; | |||
import com.google.common.collect.Maps; | |||
import java.util.Date; | |||
import java.util.LinkedList; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.server.ws.Change; | |||
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.Pagination; | |||
import org.sonar.db.qualityprofile.QProfileDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonarqube.ws.MediaTypes; | |||
import org.sonar.db.rule.RuleListQuery; | |||
import org.sonar.db.rule.RuleListResult; | |||
import org.sonar.db.rule.RuleParamDto; | |||
import org.sonar.server.rule.ws.RulesResponseFormatter.SearchResult; | |||
import org.sonarqube.ws.Common; | |||
import org.sonarqube.ws.Rules; | |||
import org.sonarqube.ws.Rules.ListResponse; | |||
import static com.google.common.base.Strings.nullToEmpty; | |||
import static java.util.stream.Collectors.toSet; | |||
import static org.sonar.api.server.ws.WebService.Param.PAGE; | |||
import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; | |||
import static org.sonar.db.rule.RuleListQuery.RuleListQueryBuilder.newRuleListQueryBuilder; | |||
import static org.sonar.server.exceptions.NotFoundException.checkFound; | |||
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_AVAILABLE_SINCE; | |||
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_QPROFILE; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
public class ListAction implements RulesWsAction { | |||
private final DbClient dbClient; | |||
private final RulesResponseFormatter rulesResponseFormatter; | |||
public ListAction(DbClient dbClient) { | |||
public ListAction(DbClient dbClient, RulesResponseFormatter rulesResponseFormatter) { | |||
this.dbClient = dbClient; | |||
this.rulesResponseFormatter = rulesResponseFormatter; | |||
} | |||
@Override | |||
public void define(WebService.NewController controller) { | |||
controller | |||
NewAction action = controller | |||
.createAction("list") | |||
.setDescription("List rules, excluding the manual rules and the rules with status REMOVED. JSON format is not supported for response.") | |||
.setDescription("List rules, excluding the external rules and the rules with status REMOVED. " + | |||
"If a quality profile is provided, it returns only the rules activated in this quality profile. " + | |||
"If no quality profile is provided, it returns all the rules available to the user.") | |||
.setSince("5.2") | |||
.setInternal(true) | |||
.setResponseExample(getClass().getResource("list-example.txt")) | |||
.setHandler(this); | |||
action.setChangelog( | |||
new Change("10.4", "'repository' field changed to 'repo'"), | |||
new Change("10.4", "Extend the response with 'actives', 'qProfiles' fields"), | |||
new Change("10.4", "Add pagination"), | |||
new Change("10.4", String.format("Add the '%s' parameter", PARAM_AVAILABLE_SINCE)), | |||
new Change("10.4", String.format("Add the '%s' parameter", PARAM_QPROFILE)), | |||
new Change("10.4", "Add the 'createdAt' sorting parameter")); | |||
action.createParam(PARAM_AVAILABLE_SINCE) | |||
.setDescription("Filter rules available since the given date. If no value is provided, all rules are returned. Format is yyyy-MM-dd.") | |||
.setExampleValue("2014-06-22") | |||
.setSince("10.4"); | |||
action.createParam(PARAM_QPROFILE) | |||
.setDescription("Filter rules that are activated in the given quality profile.") | |||
.setSince("10.4"); | |||
action.createSortParams(Set.of("createdAt"), "createdAt", false) | |||
.setSince("10.4"); | |||
action.addPagingParamsSince(100, 500, "10.4"); | |||
} | |||
@Override | |||
public void handle(Request wsRequest, Response wsResponse) throws Exception { | |||
final ListResponse.Builder listResponseBuilder = ListResponse.newBuilder(); | |||
final ListResponse.Rule.Builder ruleBuilder = ListResponse.Rule.newBuilder(); | |||
public void handle(Request request, Response response) throws Exception { | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
Set<ListResponse.Rule> rules = dbClient.ruleDao().selectEnabled(dbSession).stream() | |||
.map(dto -> toRule(ruleBuilder, dto)) | |||
.collect(toSet()); | |||
listResponseBuilder.addAllRules(rules); | |||
WsRequest wsRequest = toWsRequest(dbSession, request); | |||
SearchResult searchResult = doSearch(dbSession, wsRequest); | |||
ListResponse listResponse = buildResponse(wsRequest, dbSession, searchResult); | |||
writeProtobuf(listResponse, request, response); | |||
} | |||
} | |||
private WsRequest toWsRequest(DbSession dbSession, Request request) { | |||
WsRequest wsRequest = new WsRequest(); | |||
String sortParam = request.param(WebService.Param.SORT); | |||
if (sortParam != null) { | |||
wsRequest.setSortField(sortParam) | |||
.setAscendingSort(request.mandatoryParamAsBoolean(WebService.Param.ASCENDING)); | |||
} | |||
// JSON response is voluntarily not supported. This WS is for internal use. | |||
wsResponse.stream().setMediaType(MediaTypes.PROTOBUF); | |||
listResponseBuilder.build().writeTo(wsResponse.stream().output()); | |||
return wsRequest | |||
.setQProfile(getQProfile(dbSession, request)) | |||
.setPage(request.mandatoryParamAsInt(PAGE)) | |||
.setPageSize(request.mandatoryParamAsInt(PAGE_SIZE)) | |||
.setAvailableSince(request.paramAsDate(PARAM_AVAILABLE_SINCE)); | |||
} | |||
@Nullable | |||
private QProfileDto getQProfile(DbSession dbSession, Request request) { | |||
String profileUuid = request.param(PARAM_QPROFILE); | |||
if (profileUuid == null) { | |||
return null; | |||
} | |||
QProfileDto foundProfile = dbClient.qualityProfileDao().selectByUuid(dbSession, profileUuid); | |||
return checkFound(foundProfile, "The specified qualityProfile '%s' does not exist", profileUuid); | |||
} | |||
private SearchResult doSearch(DbSession dbSession, WsRequest wsRequest) { | |||
RuleListResult ruleListResult = dbClient.ruleDao().selectRules(dbSession, | |||
buildRuleListQuery(wsRequest), | |||
Pagination.forPage(wsRequest.page).andSize(wsRequest.pageSize)); | |||
Map<String, RuleDto> rulesByUuid = Maps.uniqueIndex(dbClient.ruleDao().selectByUuids(dbSession, ruleListResult.getUuids()), RuleDto::getUuid); | |||
Set<String> ruleUuids = rulesByUuid.keySet(); | |||
List<RuleDto> rules = new LinkedList<>(rulesByUuid.values()); | |||
List<String> templateRuleUuids = rules.stream() | |||
.map(RuleDto::getTemplateUuid) | |||
.filter(Objects::nonNull) | |||
.toList(); | |||
List<RuleDto> templateRules = dbClient.ruleDao().selectByUuids(dbSession, templateRuleUuids); | |||
List<RuleParamDto> ruleParamDtos = dbClient.ruleDao().selectRuleParamsByRuleUuids(dbSession, ruleUuids); | |||
return new SearchResult() | |||
.setRules(rules) | |||
.setRuleParameters(ruleParamDtos) | |||
.setTemplateRules(templateRules) | |||
.setTotal(ruleListResult.getTotal()); | |||
} | |||
private static ListResponse.Rule toRule(ListResponse.Rule.Builder ruleBuilder, RuleDto dto) { | |||
return ruleBuilder | |||
.clear() | |||
.setRepository(dto.getRepositoryKey()) | |||
.setKey(dto.getRuleKey()) | |||
.setName(nullToEmpty(dto.getName())) | |||
.setInternalKey(nullToEmpty(dto.getConfigKey())) | |||
private static RuleListQuery buildRuleListQuery(WsRequest wsRequest) { | |||
return newRuleListQueryBuilder() | |||
.profileUuid(wsRequest.qProfile != null ? wsRequest.qProfile.getRulesProfileUuid() : null) | |||
.createdAt(wsRequest.availableSince != null ? wsRequest.availableSince.getTime() : null) | |||
.sortField(wsRequest.sortField) | |||
.sortDirection(wsRequest.ascendingSort ? "asc" : "desc") | |||
.build(); | |||
} | |||
private ListResponse buildResponse(WsRequest wsRequest, DbSession dbSession, SearchResult searchResult) { | |||
QProfileDto qProfile = wsRequest.qProfile; | |||
Rules.Actives actives = rulesResponseFormatter.formatActiveRules(dbSession, qProfile, searchResult.getRules()); | |||
Set<String> qProfiles = actives.getActivesMap().values() | |||
.stream() | |||
.map(Rules.ActiveList::getActiveListList) | |||
.flatMap(List::stream) | |||
.map(Rules.Active::getQProfile) | |||
.collect(Collectors.toSet()); | |||
return ListResponse.newBuilder() | |||
.addAllRules(rulesResponseFormatter.formatRulesList(dbSession, searchResult)) | |||
.setActives(actives) | |||
.setQProfiles(rulesResponseFormatter.formatQualityProfiles(dbSession, qProfiles)) | |||
.setPaging(Common.Paging.newBuilder() | |||
.setPageIndex(wsRequest.page) | |||
.setPageSize(searchResult.getRules().size()) | |||
.setTotal(searchResult.getTotal().intValue()) | |||
.build()) | |||
.build(); | |||
} | |||
private static class WsRequest { | |||
private String sortField = null; | |||
private boolean ascendingSort = true; | |||
private QProfileDto qProfile = null; | |||
private int page = 1; | |||
private int pageSize = 100; | |||
private Date availableSince = null; | |||
public WsRequest setSortField(String sortField) { | |||
this.sortField = sortField; | |||
return this; | |||
} | |||
public WsRequest setAscendingSort(boolean ascendingSort) { | |||
this.ascendingSort = ascendingSort; | |||
return this; | |||
} | |||
public WsRequest setQProfile(@Nullable QProfileDto qProfile) { | |||
this.qProfile = qProfile; | |||
return this; | |||
} | |||
public WsRequest setPage(int page) { | |||
this.page = page; | |||
return this; | |||
} | |||
public WsRequest setPageSize(int pageSize) { | |||
this.pageSize = pageSize; | |||
return this; | |||
} | |||
public WsRequest setAvailableSince(@Nullable Date availableSince) { | |||
this.availableSince = availableSince; | |||
return this; | |||
} | |||
} | |||
} |
@@ -45,7 +45,7 @@ import org.sonar.db.rule.RuleParamDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.markdown.Markdown; | |||
import org.sonar.server.rule.RuleDescriptionFormatter; | |||
import org.sonar.server.rule.ws.SearchAction.SearchResult; | |||
import org.sonar.server.rule.ws.RulesResponseFormatter.SearchResult; | |||
import org.sonar.server.text.MacroInterpreter; | |||
import org.sonarqube.ws.Common; | |||
import org.sonarqube.ws.Common.RuleScope; | |||
@@ -262,8 +262,7 @@ public class RuleMapper { | |||
private static void setGapDescription(Rules.Rule.Builder ruleResponse, RuleDto ruleDto, Set<String> fieldsToReturn) { | |||
String gapDescription = ruleDto.getGapDescription(); | |||
if (shouldReturnField(fieldsToReturn, FIELD_GAP_DESCRIPTION) | |||
&& gapDescription != null) { | |||
if (shouldReturnField(fieldsToReturn, FIELD_GAP_DESCRIPTION) && gapDescription != null) { | |||
ruleResponse.setGapDescription(gapDescription); | |||
} | |||
} |
@@ -25,13 +25,15 @@ import com.google.common.collect.Lists; | |||
import com.google.common.collect.Multimap; | |||
import java.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.Collections; | |||
import java.util.HashMap; | |||
import java.util.HashSet; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import java.util.function.Function; | |||
import java.util.stream.Collectors; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.sonar.api.resources.Language; | |||
import org.sonar.api.resources.Languages; | |||
@@ -46,41 +48,95 @@ import org.sonar.db.qualityprofile.ActiveRuleKey; | |||
import org.sonar.db.qualityprofile.ActiveRuleParamDto; | |||
import org.sonar.db.qualityprofile.OrgActiveRuleDto; | |||
import org.sonar.db.qualityprofile.QProfileDto; | |||
import org.sonar.db.rule.DeprecatedRuleKeyDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonar.db.rule.RuleParamDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.es.Facets; | |||
import org.sonar.server.qualityprofile.ActiveRuleInheritance; | |||
import org.sonar.server.rule.index.RuleQuery; | |||
import org.sonarqube.ws.Rules; | |||
import org.sonarqube.ws.Rules.SearchResponse; | |||
import static com.google.common.base.Strings.nullToEmpty; | |||
import static java.util.Collections.emptyMap; | |||
import static java.util.Collections.singletonList; | |||
import static java.util.Optional.ofNullable; | |||
import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_DEPRECATED_KEYS; | |||
/** | |||
* Add details about active rules to api/rules/search and api/rules/show | |||
* web services. | |||
*/ | |||
@ServerSide | |||
public class ActiveRuleCompleter { | |||
public class RulesResponseFormatter { | |||
private final DbClient dbClient; | |||
private final RuleWsSupport ruleWsSupport; | |||
private final RuleMapper mapper; | |||
private final Languages languages; | |||
public ActiveRuleCompleter(DbClient dbClient, Languages languages) { | |||
public RulesResponseFormatter(DbClient dbClient, RuleWsSupport ruleWsSupport, RuleMapper mapper, Languages languages) { | |||
this.dbClient = dbClient; | |||
this.ruleWsSupport = ruleWsSupport; | |||
this.mapper = mapper; | |||
this.languages = languages; | |||
} | |||
void completeSearch(DbSession dbSession, RuleQuery query, List<RuleDto> rules, SearchResponse.Builder searchResponse) { | |||
Set<String> profileUuids = writeActiveRules(dbSession, searchResponse, query, rules); | |||
searchResponse.setQProfiles(buildQProfiles(dbSession, profileUuids)); | |||
public List<Rules.Rule> formatRulesSearch(DbSession dbSession, SearchResult result, Set<String> fields) { | |||
List<RuleDto> rules = result.getRules(); | |||
Map<String, UserDto> usersByUuid = ruleWsSupport.getUsersByUuid(dbSession, rules); | |||
Map<String, List<DeprecatedRuleKeyDto>> deprecatedRuleKeysByRuleUuid = getDeprecatedRuleKeysByRuleUuid(dbSession, rules, fields); | |||
return rules.stream() | |||
.map(rule -> mapper.toWsRule(rule, result, fields, usersByUuid, deprecatedRuleKeysByRuleUuid)) | |||
.toList(); | |||
} | |||
public List<Rules.Rule> formatRulesList(DbSession dbSession, SearchResult result) { | |||
Set<String> fields = Set.of("repo", "name", "severity", "lang", "internalKey", "templateKey", "params", "actives", "createdAt", "updatedAt", "deprecatedKeys", "langName"); | |||
return formatRulesSearch(dbSession, result, fields); | |||
} | |||
private Map<String, List<DeprecatedRuleKeyDto>> getDeprecatedRuleKeysByRuleUuid(DbSession dbSession, List<RuleDto> rules, Set<String> fields) { | |||
if (!RuleMapper.shouldReturnField(fields, FIELD_DEPRECATED_KEYS)) { | |||
return Collections.emptyMap(); | |||
} | |||
Set<String> ruleUuidsSet = rules.stream() | |||
.map(RuleDto::getUuid) | |||
.collect(Collectors.toSet()); | |||
if (ruleUuidsSet.isEmpty()) { | |||
return Collections.emptyMap(); | |||
} else { | |||
return dbClient.ruleDao().selectDeprecatedRuleKeysByRuleUuids(dbSession, ruleUuidsSet).stream() | |||
.collect(Collectors.groupingBy(DeprecatedRuleKeyDto::getRuleUuid)); | |||
} | |||
} | |||
private Set<String> writeActiveRules(DbSession dbSession, SearchResponse.Builder response, RuleQuery query, List<RuleDto> rules) { | |||
final Set<String> profileUuids = new HashSet<>(); | |||
Rules.Actives.Builder activesBuilder = response.getActivesBuilder(); | |||
public Rules.QProfiles formatQualityProfiles(DbSession dbSession, Set<String> profileUuids) { | |||
Rules.QProfiles.Builder result = Rules.QProfiles.newBuilder(); | |||
if (profileUuids.isEmpty()) { | |||
return result.build(); | |||
} | |||
// load profiles | |||
Map<String, QProfileDto> profilesByUuid = dbClient.qualityProfileDao().selectByUuids(dbSession, new ArrayList<>(profileUuids)) | |||
.stream() | |||
.collect(Collectors.toMap(QProfileDto::getKee, Function.identity())); | |||
// load associated parents | |||
List<String> parentUuids = profilesByUuid.values().stream() | |||
.map(QProfileDto::getParentKee) | |||
.filter(StringUtils::isNotEmpty) | |||
.filter(uuid -> !profilesByUuid.containsKey(uuid)) | |||
.toList(); | |||
if (!parentUuids.isEmpty()) { | |||
dbClient.qualityProfileDao().selectByUuids(dbSession, parentUuids) | |||
.forEach(p -> profilesByUuid.put(p.getKee(), p)); | |||
} | |||
profilesByUuid.values().forEach(p -> writeProfile(result, p)); | |||
return result.build(); | |||
} | |||
public Rules.Actives formatActiveRules(DbSession dbSession, @Nullable QProfileDto profile, List<RuleDto> rules) { | |||
Rules.Actives.Builder activesBuilder = Rules.Actives.newBuilder(); | |||
QProfileDto profile = query.getQProfile(); | |||
if (profile != null) { | |||
// Load details of active rules on the selected profile | |||
List<OrgActiveRuleDto> activeRules = dbClient.activeRuleDao().selectByProfile(dbSession, profile); | |||
@@ -91,7 +147,7 @@ public class ActiveRuleCompleter { | |||
for (RuleDto rule : rules) { | |||
OrgActiveRuleDto activeRule = activeRuleByRuleKey.get(rule.getKey()); | |||
if (activeRule != null) { | |||
profileUuids.addAll(writeActiveRules(rule.getKey(), singletonList(activeRule), activeRuleParamsByActiveRuleKey, activesBuilder)); | |||
writeActiveRules(rule.getKey(), singletonList(activeRule), activeRuleParamsByActiveRuleKey, activesBuilder); | |||
} | |||
} | |||
} else { | |||
@@ -101,25 +157,21 @@ public class ActiveRuleCompleter { | |||
Multimap<RuleKey, OrgActiveRuleDto> activeRulesByRuleKey = activeRules.stream() | |||
.collect(MoreCollectors.index(OrgActiveRuleDto::getRuleKey)); | |||
ListMultimap<ActiveRuleKey, ActiveRuleParamDto> activeRuleParamsByActiveRuleKey = loadParams(dbSession, activeRules); | |||
rules.forEach(rule -> profileUuids.addAll(writeActiveRules(rule.getKey(), activeRulesByRuleKey.get(rule.getKey()), activeRuleParamsByActiveRuleKey, activesBuilder))); | |||
rules.forEach(rule -> writeActiveRules(rule.getKey(), activeRulesByRuleKey.get(rule.getKey()), activeRuleParamsByActiveRuleKey, activesBuilder)); | |||
} | |||
response.setActives(activesBuilder); | |||
return profileUuids; | |||
return activesBuilder.build(); | |||
} | |||
private static Set<String> writeActiveRules(RuleKey ruleKey, Collection<OrgActiveRuleDto> activeRules, | |||
private static void writeActiveRules(RuleKey ruleKey, Collection<OrgActiveRuleDto> activeRules, | |||
ListMultimap<ActiveRuleKey, ActiveRuleParamDto> activeRuleParamsByActiveRuleKey, Rules.Actives.Builder activesBuilder) { | |||
final Set<String> profileUuids = new HashSet<>(); | |||
Rules.ActiveList.Builder activeRulesListResponse = Rules.ActiveList.newBuilder(); | |||
for (OrgActiveRuleDto activeRule : activeRules) { | |||
activeRulesListResponse.addActiveList(buildActiveRuleResponse(activeRule, activeRuleParamsByActiveRuleKey.get(activeRule.getKey()))); | |||
profileUuids.add(activeRule.getOrgProfileUuid()); | |||
} | |||
activesBuilder | |||
.getMutableActives() | |||
.put(ruleKey.toString(), activeRulesListResponse.build()); | |||
return profileUuids; | |||
} | |||
private ListMultimap<ActiveRuleKey, ActiveRuleParamDto> loadParams(DbSession dbSession, List<OrgActiveRuleDto> activeRules) { | |||
@@ -137,7 +189,7 @@ public class ActiveRuleCompleter { | |||
return activeRuleParamsByActiveRuleKey; | |||
} | |||
List<Rules.Active> completeShow(DbSession dbSession, RuleDto rule) { | |||
public List<Rules.Active> formatActiveRule(DbSession dbSession, RuleDto rule) { | |||
List<OrgActiveRuleDto> activeRules = dbClient.activeRuleDao().selectByOrgRuleUuid(dbSession, rule.getUuid()); | |||
Map<String, ActiveRuleKey> activeRuleUuidsByKey = new HashMap<>(); | |||
for (OrgActiveRuleDto activeRuleDto : activeRules) { | |||
@@ -175,35 +227,7 @@ public class ActiveRuleCompleter { | |||
return builder.build(); | |||
} | |||
private Rules.QProfiles.Builder buildQProfiles(DbSession dbSession, Set<String> profileUuids) { | |||
Rules.QProfiles.Builder result = Rules.QProfiles.newBuilder(); | |||
if (profileUuids.isEmpty()) { | |||
return result; | |||
} | |||
// load profiles | |||
Map<String, QProfileDto> profilesByUuid = dbClient.qualityProfileDao().selectByUuids(dbSession, new ArrayList<>(profileUuids)) | |||
.stream() | |||
.collect(Collectors.toMap(QProfileDto::getKee, Function.identity())); | |||
// load associated parents | |||
List<String> parentUuids = profilesByUuid.values().stream() | |||
.map(QProfileDto::getParentKee) | |||
.filter(StringUtils::isNotEmpty) | |||
.filter(uuid -> !profilesByUuid.containsKey(uuid)) | |||
.toList(); | |||
if (!parentUuids.isEmpty()) { | |||
dbClient.qualityProfileDao().selectByUuids(dbSession, parentUuids) | |||
.forEach(p -> profilesByUuid.put(p.getKee(), p)); | |||
} | |||
Map<String, Rules.QProfile> qProfilesMapResponse = result.getMutableQProfiles(); | |||
profilesByUuid.values().forEach(p -> writeProfile(qProfilesMapResponse, p)); | |||
return result; | |||
} | |||
private void writeProfile(Map<String, Rules.QProfile> profilesResponse, QProfileDto profile) { | |||
private void writeProfile(Rules.QProfiles.Builder profilesResponse, QProfileDto profile) { | |||
Rules.QProfile.Builder profileResponse = Rules.QProfile.newBuilder(); | |||
ofNullable(profile.getName()).ifPresent(profileResponse::setName); | |||
@@ -215,6 +239,78 @@ public class ActiveRuleCompleter { | |||
} | |||
ofNullable(profile.getParentKee()).ifPresent(profileResponse::setParent); | |||
profilesResponse.put(profile.getKee(), profileResponse.build()); | |||
profilesResponse.putQProfiles(profile.getKee(), profileResponse.build()); | |||
} | |||
public Rules.Rule formatRule(DbSession dbSession, SearchResult searchResult) { | |||
RuleDto rule = searchResult.getRules().get(0); | |||
return mapper.toWsRule(rule, searchResult, Collections.emptySet(), | |||
ruleWsSupport.getUsersByUuid(dbSession, searchResult.getRules()), emptyMap()); | |||
} | |||
static class SearchResult { | |||
private List<RuleDto> rules; | |||
private final ListMultimap<String, RuleParamDto> ruleParamsByRuleUuid; | |||
private final Map<String, RuleDto> templateRulesByRuleUuid; | |||
private Long total; | |||
private Facets facets; | |||
public SearchResult() { | |||
this.rules = new ArrayList<>(); | |||
this.ruleParamsByRuleUuid = ArrayListMultimap.create(); | |||
this.templateRulesByRuleUuid = new HashMap<>(); | |||
} | |||
public List<RuleDto> getRules() { | |||
return rules; | |||
} | |||
public SearchResult setRules(List<RuleDto> rules) { | |||
this.rules = rules; | |||
return this; | |||
} | |||
public ListMultimap<String, RuleParamDto> getRuleParamsByRuleUuid() { | |||
return ruleParamsByRuleUuid; | |||
} | |||
public SearchResult setRuleParameters(List<RuleParamDto> ruleParams) { | |||
ruleParamsByRuleUuid.clear(); | |||
for (RuleParamDto ruleParam : ruleParams) { | |||
ruleParamsByRuleUuid.put(ruleParam.getRuleUuid(), ruleParam); | |||
} | |||
return this; | |||
} | |||
public Map<String, RuleDto> getTemplateRulesByRuleUuid() { | |||
return templateRulesByRuleUuid; | |||
} | |||
public SearchResult setTemplateRules(List<RuleDto> templateRules) { | |||
templateRulesByRuleUuid.clear(); | |||
for (RuleDto templateRule : templateRules) { | |||
templateRulesByRuleUuid.put(templateRule.getUuid(), templateRule); | |||
} | |||
return this; | |||
} | |||
public Long getTotal() { | |||
return total; | |||
} | |||
public SearchResult setTotal(Long total) { | |||
this.total = total; | |||
return this; | |||
} | |||
@CheckForNull | |||
public Facets getFacets() { | |||
return facets; | |||
} | |||
public SearchResult setFacets(Facets facets) { | |||
this.facets = facets; | |||
return this; | |||
} | |||
} | |||
} |
@@ -19,24 +19,20 @@ | |||
*/ | |||
package org.sonar.server.rule.ws; | |||
import com.google.common.collect.ArrayListMultimap; | |||
import com.google.common.collect.ListMultimap; | |||
import com.google.common.collect.Lists; | |||
import com.google.common.collect.Maps; | |||
import com.google.common.collect.Ordering; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.Collection; | |||
import java.util.Collections; | |||
import java.util.HashMap; | |||
import java.util.HashSet; | |||
import java.util.Iterator; | |||
import java.util.LinkedList; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.issue.impact.SoftwareQuality; | |||
import org.sonar.api.rule.Severity; | |||
@@ -48,16 +44,16 @@ import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.rule.DeprecatedRuleKeyDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonar.db.rule.RuleParamDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.es.Facets; | |||
import org.sonar.server.es.SearchIdResult; | |||
import org.sonar.server.es.SearchOptions; | |||
import org.sonar.server.rule.index.RuleIndex; | |||
import org.sonar.server.rule.index.RuleQuery; | |||
import org.sonar.server.rule.ws.RulesResponseFormatter.SearchResult; | |||
import org.sonarqube.ws.Common; | |||
import org.sonarqube.ws.Rules; | |||
import org.sonarqube.ws.Rules.SearchResponse; | |||
import static java.lang.String.format; | |||
@@ -84,7 +80,6 @@ import static org.sonar.server.rule.index.RuleIndex.FACET_SONARSOURCE_SECURITY; | |||
import static org.sonar.server.rule.index.RuleIndex.FACET_STATUSES; | |||
import static org.sonar.server.rule.index.RuleIndex.FACET_TAGS; | |||
import static org.sonar.server.rule.index.RuleIndex.FACET_TYPES; | |||
import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_DEPRECATED_KEYS; | |||
import static org.sonar.server.rule.ws.RulesWsParameters.OPTIONAL_FIELDS; | |||
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_ACTIVE_SEVERITIES; | |||
import static org.sonar.server.rule.ws.RulesWsParameters.PARAM_CLEAN_CODE_ATTRIBUTE_CATEGORIES; | |||
@@ -107,7 +102,7 @@ public class SearchAction implements RulesWsAction { | |||
public static final String ACTION = "search"; | |||
private static final Collection<String> DEFAULT_FACETS = Set.of(PARAM_LANGUAGES, PARAM_REPOSITORIES, "tags"); | |||
private static final String[] POSSIBLE_FACETS = new String[]{ | |||
private static final String[] POSSIBLE_FACETS = new String[] { | |||
FACET_LANGUAGES, | |||
FACET_REPOSITORIES, | |||
FACET_TAGS, | |||
@@ -129,18 +124,13 @@ public class SearchAction implements RulesWsAction { | |||
private final RuleQueryFactory ruleQueryFactory; | |||
private final DbClient dbClient; | |||
private final RuleIndex ruleIndex; | |||
private final ActiveRuleCompleter activeRuleCompleter; | |||
private final RuleMapper mapper; | |||
private final RuleWsSupport ruleWsSupport; | |||
private final RulesResponseFormatter rulesResponseFormatter; | |||
public SearchAction(RuleIndex ruleIndex, ActiveRuleCompleter activeRuleCompleter, RuleQueryFactory ruleQueryFactory, DbClient dbClient, RuleMapper mapper, | |||
RuleWsSupport ruleWsSupport) { | |||
public SearchAction(RuleIndex ruleIndex, RuleQueryFactory ruleQueryFactory, DbClient dbClient, RulesResponseFormatter rulesResponseFormatter) { | |||
this.ruleIndex = ruleIndex; | |||
this.activeRuleCompleter = activeRuleCompleter; | |||
this.ruleQueryFactory = ruleQueryFactory; | |||
this.dbClient = dbClient; | |||
this.mapper = mapper; | |||
this.ruleWsSupport = ruleWsSupport; | |||
this.rulesResponseFormatter = rulesResponseFormatter; | |||
} | |||
@Override | |||
@@ -236,10 +226,10 @@ public class SearchAction implements RulesWsAction { | |||
} | |||
private static void writeStatistics(SearchResponse.Builder response, SearchResult searchResult, SearchOptions context) { | |||
response.setTotal(searchResult.total); | |||
response.setTotal(searchResult.getTotal()); | |||
response.setP(context.getPage()); | |||
response.setPs(context.getLimit()); | |||
response.setPaging(formatPaging(searchResult.total, context.getPage(), context.getLimit())); | |||
response.setPaging(formatPaging(searchResult.getTotal(), context.getPage(), context.getLimit())); | |||
} | |||
private static Common.Paging.Builder formatPaging(Long total, int pageIndex, int limit) { | |||
@@ -249,29 +239,6 @@ public class SearchAction implements RulesWsAction { | |||
.setTotal(total.intValue()); | |||
} | |||
private void writeRules(DbSession dbSession, SearchResponse.Builder response, SearchResult result, SearchOptions context) { | |||
Map<String, UserDto> usersByUuid = ruleWsSupport.getUsersByUuid(dbSession, result.rules); | |||
Map<String, List<DeprecatedRuleKeyDto>> deprecatedRuleKeysByRuleUuid = getDeprecatedRuleKeysByRuleUuid(dbSession, result.rules, context); | |||
result.rules.forEach(rule -> response.addRules(mapper.toWsRule(rule, result, context.getFields(), usersByUuid, | |||
deprecatedRuleKeysByRuleUuid))); | |||
} | |||
private Map<String, List<DeprecatedRuleKeyDto>> getDeprecatedRuleKeysByRuleUuid(DbSession dbSession, List<RuleDto> rules, SearchOptions context) { | |||
if (!RuleMapper.shouldReturnField(context.getFields(), FIELD_DEPRECATED_KEYS)) { | |||
return Collections.emptyMap(); | |||
} | |||
Set<String> ruleUuidsSet = rules.stream() | |||
.map(RuleDto::getUuid) | |||
.collect(Collectors.toSet()); | |||
if (ruleUuidsSet.isEmpty()) { | |||
return Collections.emptyMap(); | |||
} else { | |||
return dbClient.ruleDao().selectDeprecatedRuleKeysByRuleUuids(dbSession, ruleUuidsSet).stream() | |||
.collect(Collectors.groupingBy(DeprecatedRuleKeyDto::getRuleUuid)); | |||
} | |||
} | |||
private static SearchOptions buildSearchOptions(SearchRequest request) { | |||
SearchOptions context = loadCommonContext(request); | |||
SearchOptions searchOptions = new SearchOptions() | |||
@@ -304,7 +271,7 @@ public class SearchAction implements RulesWsAction { | |||
List<String> ruleUuids = result.getUuids(); | |||
// rule order is managed by ES, this order by must be kept when fetching rule details | |||
Map<String, RuleDto> rulesByRuleKey = Maps.uniqueIndex(dbClient.ruleDao().selectByUuids(dbSession, ruleUuids), RuleDto::getUuid); | |||
List<RuleDto> rules = new ArrayList<>(); | |||
List<RuleDto> rules = new LinkedList<>(); | |||
for (String ruleUuid : ruleUuids) { | |||
RuleDto rule = rulesByRuleKey.get(ruleUuid); | |||
if (rule != null) { | |||
@@ -328,13 +295,27 @@ public class SearchAction implements RulesWsAction { | |||
private void doContextResponse(DbSession dbSession, SearchRequest request, SearchResult result, SearchResponse.Builder response, RuleQuery query) { | |||
SearchOptions contextForResponse = loadCommonContext(request); | |||
writeRules(dbSession, response, result, contextForResponse); | |||
response.addAllRules(rulesResponseFormatter.formatRulesSearch(dbSession, result, contextForResponse.getFields())); | |||
if (contextForResponse.getFields().contains("actives")) { | |||
activeRuleCompleter.completeSearch(dbSession, query, result.rules, response); | |||
Rules.Actives actives = rulesResponseFormatter.formatActiveRules(dbSession, query.getQProfile(), result.getRules()); | |||
Set<String> qProfiles = actives.getActivesMap().values() | |||
.stream() | |||
.map(Rules.ActiveList::getActiveListList) | |||
.flatMap(List::stream) | |||
.map(Rules.Active::getQProfile) | |||
.collect(Collectors.toSet()); | |||
Rules.QProfiles profiles = rulesResponseFormatter.formatQualityProfiles(dbSession, qProfiles); | |||
response.setActives(actives); | |||
response.setQProfiles(profiles); | |||
} | |||
} | |||
private static void writeFacets(SearchResponse.Builder response, SearchRequest request, SearchOptions context, SearchResult results) { | |||
Facets resultsFacets = results.getFacets(); | |||
if (resultsFacets == null) { | |||
return; | |||
} | |||
addMandatoryFacetValues(results, FACET_LANGUAGES, request.getLanguages()); | |||
addMandatoryFacetValues(results, FACET_REPOSITORIES, request.getRepositories()); | |||
addMandatoryFacetValues(results, FACET_STATUSES, ALL_STATUSES_EXCEPT_REMOVED); | |||
@@ -372,7 +353,7 @@ public class SearchAction implements RulesWsAction { | |||
for (String facetName : context.getFacets()) { | |||
facet.clear().setProperty(facetName); | |||
Map<String, Long> facets = results.facets.get(facetName); | |||
Map<String, Long> facets = resultsFacets.get(facetName); | |||
if (facets != null) { | |||
Set<String> itemsFromFacets = new HashSet<>(); | |||
for (Map.Entry<String, Long> facetValue : facets.entrySet()) { | |||
@@ -406,7 +387,11 @@ public class SearchAction implements RulesWsAction { | |||
} | |||
private static void addMandatoryFacetValues(SearchResult results, String facetName, @Nullable Collection<String> mandatoryValues) { | |||
Map<String, Long> facetValues = results.facets.get(facetName); | |||
Facets facets = results.getFacets(); | |||
if (facets == null) { | |||
return; | |||
} | |||
Map<String, Long> facetValues = facets.get(facetName); | |||
if (facetValues != null) { | |||
Collection<String> valuesToAdd = mandatoryValues == null ? Lists.newArrayList() : mandatoryValues; | |||
for (String item : valuesToAdd) { | |||
@@ -441,73 +426,6 @@ public class SearchAction implements RulesWsAction { | |||
.setSonarsourceSecurity(request.paramAsStrings(PARAM_SONARSOURCE_SECURITY)); | |||
} | |||
static class SearchResult { | |||
private List<RuleDto> rules; | |||
private final ListMultimap<String, RuleParamDto> ruleParamsByRuleUuid; | |||
private final Map<String, RuleDto> templateRulesByRuleUuid; | |||
private Long total; | |||
private Facets facets; | |||
public SearchResult() { | |||
this.rules = new ArrayList<>(); | |||
this.ruleParamsByRuleUuid = ArrayListMultimap.create(); | |||
this.templateRulesByRuleUuid = new HashMap<>(); | |||
} | |||
public List<RuleDto> getRules() { | |||
return rules; | |||
} | |||
public SearchResult setRules(List<RuleDto> rules) { | |||
this.rules = rules; | |||
return this; | |||
} | |||
public ListMultimap<String, RuleParamDto> getRuleParamsByRuleUuid() { | |||
return ruleParamsByRuleUuid; | |||
} | |||
public SearchResult setRuleParameters(List<RuleParamDto> ruleParams) { | |||
ruleParamsByRuleUuid.clear(); | |||
for (RuleParamDto ruleParam : ruleParams) { | |||
ruleParamsByRuleUuid.put(ruleParam.getRuleUuid(), ruleParam); | |||
} | |||
return this; | |||
} | |||
public Map<String, RuleDto> getTemplateRulesByRuleUuid() { | |||
return templateRulesByRuleUuid; | |||
} | |||
public SearchResult setTemplateRules(List<RuleDto> templateRules) { | |||
templateRulesByRuleUuid.clear(); | |||
for (RuleDto templateRule : templateRules) { | |||
templateRulesByRuleUuid.put(templateRule.getUuid(), templateRule); | |||
} | |||
return this; | |||
} | |||
@CheckForNull | |||
public Long getTotal() { | |||
return total; | |||
} | |||
public SearchResult setTotal(Long total) { | |||
this.total = total; | |||
return this; | |||
} | |||
@CheckForNull | |||
public Facets getFacets() { | |||
return facets; | |||
} | |||
public SearchResult setFacets(Facets facets) { | |||
this.facets = facets; | |||
return this; | |||
} | |||
} | |||
private static class SearchRequest { | |||
private List<String> activeSeverities; | |||
@@ -530,7 +448,6 @@ public class SearchAction implements RulesWsAction { | |||
private List<String> impactSoftwareQualities; | |||
private List<String> cleanCodeAttributesCategories; | |||
private SearchRequest setActiveSeverities(List<String> activeSeverities) { | |||
this.activeSeverities = activeSeverities; | |||
return this; |
@@ -35,7 +35,6 @@ import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonarqube.ws.Rules.ShowResponse; | |||
import static java.lang.String.format; | |||
import static java.util.Collections.emptyMap; | |||
import static java.util.Collections.singletonList; | |||
import static java.util.Optional.ofNullable; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
@@ -49,15 +48,11 @@ public class ShowAction implements RulesWsAction { | |||
public static final String PARAM_ACTIVES = "actives"; | |||
private final DbClient dbClient; | |||
private final RuleMapper mapper; | |||
private final ActiveRuleCompleter activeRuleCompleter; | |||
private final RuleWsSupport ruleWsSupport; | |||
private final RulesResponseFormatter rulesResponseFormatter; | |||
public ShowAction(DbClient dbClient, RuleMapper mapper, ActiveRuleCompleter activeRuleCompleter, RuleWsSupport ruleWsSupport) { | |||
public ShowAction(DbClient dbClient, RulesResponseFormatter rulesResponseFormatter) { | |||
this.dbClient = dbClient; | |||
this.activeRuleCompleter = activeRuleCompleter; | |||
this.mapper = mapper; | |||
this.ruleWsSupport = ruleWsSupport; | |||
this.rulesResponseFormatter = rulesResponseFormatter; | |||
} | |||
@Override | |||
@@ -117,7 +112,7 @@ public class ShowAction implements RulesWsAction { | |||
List<RuleParamDto> ruleParameters = dbClient.ruleDao().selectRuleParamsByRuleUuids(dbSession, singletonList(rule.getUuid())); | |||
ShowResponse showResponse = buildResponse(dbSession, request, | |||
new SearchAction.SearchResult() | |||
new RulesResponseFormatter.SearchResult() | |||
.setRules(singletonList(rule)) | |||
.setTemplateRules(templateRules) | |||
.setRuleParameters(ruleParameters) | |||
@@ -126,13 +121,12 @@ public class ShowAction implements RulesWsAction { | |||
} | |||
} | |||
private ShowResponse buildResponse(DbSession dbSession, Request request, SearchAction.SearchResult searchResult) { | |||
private ShowResponse buildResponse(DbSession dbSession, Request request, RulesResponseFormatter.SearchResult searchResult) { | |||
ShowResponse.Builder responseBuilder = ShowResponse.newBuilder(); | |||
RuleDto rule = searchResult.getRules().get(0); | |||
responseBuilder.setRule(mapper.toWsRule(rule, searchResult, Collections.emptySet(), | |||
ruleWsSupport.getUsersByUuid(dbSession, searchResult.getRules()), emptyMap())); | |||
responseBuilder.setRule(rulesResponseFormatter.formatRule(dbSession, searchResult)); | |||
if (request.mandatoryParamAsBoolean(PARAM_ACTIVES)) { | |||
activeRuleCompleter.completeShow(dbSession, rule).forEach(responseBuilder::addActives); | |||
responseBuilder.addAllActives(rulesResponseFormatter.formatActiveRule(dbSession, rule)); | |||
} | |||
return responseBuilder.build(); | |||
} |
@@ -250,7 +250,7 @@ public class UpdateAction implements RulesWsAction { | |||
} | |||
List<RuleParamDto> ruleParameters = dbClient.ruleDao().selectRuleParamsByRuleUuids(dbSession, singletonList(rule.getUuid())); | |||
UpdateResponse.Builder responseBuilder = UpdateResponse.newBuilder(); | |||
SearchAction.SearchResult searchResult = new SearchAction.SearchResult() | |||
RulesResponseFormatter.SearchResult searchResult = new RulesResponseFormatter.SearchResult() | |||
.setRules(singletonList(rule)) | |||
.setTemplateRules(templateRules) | |||
.setRuleParameters(ruleParameters) |
@@ -227,11 +227,11 @@ import org.sonar.server.rule.RuleUpdater; | |||
import org.sonar.server.rule.WebServerRuleFinderImpl; | |||
import org.sonar.server.rule.index.RuleIndexDefinition; | |||
import org.sonar.server.rule.index.RuleIndexer; | |||
import org.sonar.server.rule.ws.ActiveRuleCompleter; | |||
import org.sonar.server.rule.ws.RepositoriesAction; | |||
import org.sonar.server.rule.ws.RuleMapper; | |||
import org.sonar.server.rule.ws.RuleQueryFactory; | |||
import org.sonar.server.rule.ws.RuleWsSupport; | |||
import org.sonar.server.rule.ws.RulesResponseFormatter; | |||
import org.sonar.server.rule.ws.RulesWs; | |||
import org.sonar.server.rule.ws.TagsAction; | |||
import org.sonar.server.saml.ws.SamlValidationModule; | |||
@@ -354,7 +354,7 @@ public class PlatformLevel4 extends PlatformLevel { | |||
org.sonar.server.rule.ws.ListAction.class, | |||
TagsAction.class, | |||
RuleMapper.class, | |||
ActiveRuleCompleter.class, | |||
RulesResponseFormatter.class, | |||
RepositoriesAction.class, | |||
RuleQueryFactory.class, | |||
org.sonar.server.rule.ws.AppAction.class, |
@@ -80,7 +80,7 @@ import org.sonar.scanner.scan.branch.BranchType; | |||
import org.sonar.scanner.scan.branch.ProjectBranches; | |||
import org.sonarqube.ws.NewCodePeriods; | |||
import org.sonarqube.ws.Qualityprofiles.SearchWsResponse.QualityProfile; | |||
import org.sonarqube.ws.Rules.ListResponse.Rule; | |||
import org.sonarqube.ws.Rules.Rule; | |||
import static java.util.Collections.emptySet; | |||
@@ -168,7 +168,7 @@ public class ScannerMediumTester extends ExternalResource { | |||
public ScannerMediumTester addRule(String key, String repoKey, String internalKey, String name) { | |||
Rule.Builder builder = Rule.newBuilder(); | |||
builder.setKey(key); | |||
builder.setRepository(repoKey); | |||
builder.setRepo(repoKey); | |||
if (internalKey != null) { | |||
builder.setInternalKey(internalKey); | |||
} |
@@ -31,19 +31,18 @@ import org.sonar.api.batch.rule.LoadedActiveRule; | |||
import org.sonar.api.impl.utils.ScannerUtils; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.utils.DateUtils; | |||
import org.sonar.api.utils.MessageException; | |||
import org.sonar.scanner.bootstrap.ScannerWsClient; | |||
import org.sonarqube.ws.Common.Paging; | |||
import org.sonarqube.ws.Rules; | |||
import org.sonarqube.ws.Rules.Active; | |||
import org.sonarqube.ws.Rules.Active.Param; | |||
import org.sonarqube.ws.Rules.ActiveList; | |||
import org.sonarqube.ws.Rules.ListResponse; | |||
import org.sonarqube.ws.Rules.Rule; | |||
import org.sonarqube.ws.Rules.SearchResponse; | |||
import org.sonarqube.ws.client.GetRequest; | |||
public class DefaultActiveRulesLoader implements ActiveRulesLoader { | |||
private static final String RULES_SEARCH_URL = "/api/rules/search.protobuf?" + | |||
"f=repo,name,severity,lang,internalKey,templateKey,params,actives,createdAt,updatedAt,deprecatedKeys&activation=true"; | |||
private static final String RULES_SEARCH_URL = "/api/rules/list.protobuf?"; | |||
private final ScannerWsClient wsClient; | |||
@@ -60,12 +59,14 @@ public class DefaultActiveRulesLoader implements ActiveRulesLoader { | |||
while (true) { | |||
GetRequest getRequest = new GetRequest(getUrl(qualityProfileKey, page, pageSize)); | |||
SearchResponse response = loadFromStream(wsClient.call(getRequest).contentStream()); | |||
ListResponse response = loadFromStream(wsClient.call(getRequest).contentStream()); | |||
List<LoadedActiveRule> pageRules = readPage(response); | |||
ruleList.addAll(pageRules); | |||
loaded += response.getPs(); | |||
if (response.getTotal() <= loaded) { | |||
Paging paging = response.getPaging(); | |||
loaded += paging.getPageSize(); | |||
if (paging.getTotal() <= loaded) { | |||
break; | |||
} | |||
page++; | |||
@@ -77,15 +78,15 @@ public class DefaultActiveRulesLoader implements ActiveRulesLoader { | |||
private static String getUrl(String qualityProfileKey, int page, int pageSize) { | |||
StringBuilder builder = new StringBuilder(1024); | |||
builder.append(RULES_SEARCH_URL); | |||
builder.append("&qprofile=").append(ScannerUtils.encodeForUrl(qualityProfileKey)); | |||
builder.append("qprofile=").append(ScannerUtils.encodeForUrl(qualityProfileKey)); | |||
builder.append("&ps=").append(pageSize); | |||
builder.append("&p=").append(page); | |||
return builder.toString(); | |||
} | |||
private static SearchResponse loadFromStream(InputStream is) { | |||
private static ListResponse loadFromStream(InputStream is) { | |||
try { | |||
return SearchResponse.parseFrom(is); | |||
return ListResponse.parseFrom(is); | |||
} catch (IOException e) { | |||
throw new IllegalStateException("Failed to load quality profiles", e); | |||
} finally { | |||
@@ -93,7 +94,7 @@ public class DefaultActiveRulesLoader implements ActiveRulesLoader { | |||
} | |||
} | |||
private static List<LoadedActiveRule> readPage(SearchResponse response) { | |||
private static List<LoadedActiveRule> readPage(ListResponse response) { | |||
List<LoadedActiveRule> loadedRules = new LinkedList<>(); | |||
List<Rule> rulesList = response.getRulesList(); | |||
@@ -101,10 +102,6 @@ public class DefaultActiveRulesLoader implements ActiveRulesLoader { | |||
for (Rule r : rulesList) { | |||
ActiveList activeList = actives.get(r.getKey()); | |||
if (activeList == null) { | |||
throw MessageException.of("Elasticsearch indices have become inconsistent. Consider re-indexing. " + | |||
"Check documentation for more information https://docs.sonarsource.com/sonarqube/latest/setup/troubleshooting"); | |||
} | |||
Active active = activeList.getActiveList(0); | |||
LoadedActiveRule loadedRule = new LoadedActiveRule(); |
@@ -20,8 +20,8 @@ | |||
package org.sonar.scanner.rule; | |||
import java.util.List; | |||
import org.sonarqube.ws.Rules.ListResponse.Rule; | |||
import org.sonarqube.ws.Rules; | |||
public interface RulesLoader { | |||
List<Rule> load(); | |||
List<Rules.Rule> load(); | |||
} |
@@ -19,30 +19,27 @@ | |||
*/ | |||
package org.sonar.scanner.rule; | |||
import com.google.common.collect.ImmutableSortedMap; | |||
import java.io.ByteArrayInputStream; | |||
import java.io.InputStream; | |||
import java.util.Collection; | |||
import java.util.Map; | |||
import java.util.stream.IntStream; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.sonar.api.batch.rule.LoadedActiveRule; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.rule.Severity; | |||
import org.sonar.api.utils.MessageException; | |||
import org.sonar.scanner.WsTestUtil; | |||
import org.sonar.scanner.bootstrap.DefaultScannerWsClient; | |||
import org.sonar.scanner.scan.branch.BranchConfiguration; | |||
import org.sonarqube.ws.Common; | |||
import org.sonarqube.ws.Rules; | |||
import org.sonarqube.ws.Rules.Active; | |||
import org.sonarqube.ws.Rules.ActiveList; | |||
import org.sonarqube.ws.Rules.Actives; | |||
import org.sonarqube.ws.Rules.Rule; | |||
import org.sonarqube.ws.Rules.SearchResponse; | |||
import org.sonarqube.ws.Rules.SearchResponse.Builder; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.verifyNoMoreInteractions; | |||
import static org.mockito.Mockito.when; | |||
@@ -68,11 +65,11 @@ public class DefaultActiveRulesLoaderTest { | |||
} | |||
@Test | |||
public void feed_real_response_encode_qp() { | |||
public void load_shouldRequestRulesAndParseResponse() { | |||
int total = PAGE_SIZE_1 + PAGE_SIZE_2; | |||
WsTestUtil.mockStream(wsClient, urlOfPage(1), responseOfSize(PAGE_SIZE_1, total)); | |||
WsTestUtil.mockStream(wsClient, urlOfPage(2), responseOfSize(PAGE_SIZE_2, total)); | |||
WsTestUtil.mockStream(wsClient, urlOfPage(1), responseOfSize(1, PAGE_SIZE_1, total)); | |||
WsTestUtil.mockStream(wsClient, urlOfPage(2), responseOfSize(2, PAGE_SIZE_2, total)); | |||
Collection<LoadedActiveRule> activeRules = loader.load("c+-test_c+-values-17445"); | |||
assertThat(activeRules).hasSize(total); | |||
@@ -92,29 +89,20 @@ public class DefaultActiveRulesLoaderTest { | |||
verifyNoMoreInteractions(wsClient); | |||
} | |||
@Test | |||
public void exception_thrown_when_elasticsearch_index_inconsistent() { | |||
WsTestUtil.mockStream(wsClient, urlOfPage(1), prepareCorruptedResponse()); | |||
assertThatThrownBy(() -> loader.load("c+-test_c+-values-17445")) | |||
.isInstanceOf(MessageException.class) | |||
.hasMessage("Elasticsearch indices have become inconsistent. Consider re-indexing. " + | |||
"Check documentation for more information https://docs.sonarsource.com/sonarqube/latest/setup/troubleshooting"); | |||
} | |||
private String urlOfPage(int page) { | |||
return "/api/rules/search.protobuf?f=repo,name,severity,lang,internalKey,templateKey,params,actives,createdAt,updatedAt,deprecatedKeys&activation=true" | |||
+ ("") + "&qprofile=c%2B-test_c%2B-values-17445&ps=500&p=" + page + ""; | |||
return "/api/rules/list.protobuf?qprofile=c%2B-test_c%2B-values-17445&ps=500&p=" + page + ""; | |||
} | |||
/** | |||
* Generates an imaginary protobuf result. | |||
* | |||
* @param pageIndex page index, that the response should contain | |||
* @param numberOfRules the number of rules, that the response should contain | |||
* @param total the number of results on all pages | |||
* @return the binary stream | |||
*/ | |||
private InputStream responseOfSize(int numberOfRules, int total) { | |||
Builder rules = SearchResponse.newBuilder(); | |||
private InputStream responseOfSize(int pageIndex, int numberOfRules, int total) { | |||
Rules.ListResponse.Builder rules = Rules.ListResponse.newBuilder(); | |||
Actives.Builder actives = Actives.newBuilder(); | |||
IntStream.rangeClosed(1, numberOfRules) | |||
@@ -133,31 +121,15 @@ public class DefaultActiveRulesLoaderTest { | |||
activeBuilder.setSeverity(SEVERITY_VALUE); | |||
} | |||
ActiveList activeList = Rules.ActiveList.newBuilder().addActiveList(activeBuilder).build(); | |||
actives.putAllActives(ImmutableSortedMap.of(key.toString(), activeList)); | |||
}); | |||
rules.setActives(actives); | |||
rules.setPs(numberOfRules); | |||
rules.setTotal(total); | |||
return new ByteArrayInputStream(rules.build().toByteArray()); | |||
} | |||
private InputStream prepareCorruptedResponse() { | |||
Builder rules = SearchResponse.newBuilder(); | |||
Actives.Builder actives = Actives.newBuilder(); | |||
IntStream.rangeClosed(1, 3) | |||
.mapToObj(i -> RuleKey.of("java", "S" + i)) | |||
.forEach(key -> { | |||
Rule.Builder ruleBuilder = Rule.newBuilder(); | |||
ruleBuilder.setKey(key.toString()); | |||
rules.addRules(ruleBuilder); | |||
actives.putAllActives(Map.of(key.toString(), activeList)); | |||
}); | |||
rules.setActives(actives); | |||
rules.setPs(3); | |||
rules.setTotal(3); | |||
rules.setPaging(Common.Paging.newBuilder() | |||
.setTotal(total) | |||
.setPageIndex(pageIndex) | |||
.setPageSize(numberOfRules) | |||
.build()); | |||
return new ByteArrayInputStream(rules.build().toByteArray()); | |||
} | |||
} |
@@ -29,14 +29,14 @@ option optimize_for = SPEED; | |||
// WS api/rules/list for internal use only | |||
message ListResponse { | |||
message Rule { | |||
optional string repository = 1; | |||
optional string key = 2; | |||
optional string internal_key = 3; | |||
optional string name = 4; | |||
} | |||
repeated Rule rules = 1; | |||
optional Actives actives = 2; | |||
optional QProfiles qProfiles = 3; | |||
optional sonarqube.ws.commons.Paging paging = 4; | |||
} | |||
// WS api/rules/search |