diff options
20 files changed, 1153 insertions, 191 deletions
diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java index 4de5acb9b21..6b8ee0ac168 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java @@ -128,7 +128,7 @@ public class ComputeEngineContainerImplTest { assertThat(picoContainer.getParent().getParent().getParent().getComponentAdapters()).hasSize( COMPONENTS_IN_LEVEL_1_AT_CONSTRUCTION + 26 // level 1 - + 61 // content of DaoModule + + 62 // content of DaoModule + 3 // content of EsModule + 52 // content of CorePropertyDefinitions + 1 // StopFlagContainer diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java b/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java index 3f833346883..93a5d217467 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java @@ -71,6 +71,7 @@ import org.sonar.db.qualityprofile.QProfileChangeDao; import org.sonar.db.qualityprofile.QProfileEditGroupsDao; import org.sonar.db.qualityprofile.QProfileEditUsersDao; import org.sonar.db.qualityprofile.QualityProfileDao; +import org.sonar.db.qualityprofile.QualityProfileExportDao; import org.sonar.db.rule.RuleDao; import org.sonar.db.rule.RuleRepositoryDao; import org.sonar.db.schemamigration.SchemaMigrationDao; @@ -139,6 +140,7 @@ public class DaoModule extends Module { QualityGateConditionDao.class, QualityGateDao.class, QualityProfileDao.class, + QualityProfileExportDao.class, RoleDao.class, RuleDao.class, RuleRepositoryDao.class, diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java b/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java index eb220ed54c0..27f1832a2bb 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java @@ -69,6 +69,7 @@ import org.sonar.db.qualityprofile.QProfileChangeDao; import org.sonar.db.qualityprofile.QProfileEditGroupsDao; import org.sonar.db.qualityprofile.QProfileEditUsersDao; import org.sonar.db.qualityprofile.QualityProfileDao; +import org.sonar.db.qualityprofile.QualityProfileExportDao; import org.sonar.db.rule.RuleDao; import org.sonar.db.rule.RuleRepositoryDao; import org.sonar.db.schemamigration.SchemaMigrationDao; @@ -94,6 +95,7 @@ public class DbClient { private final OrganizationDao organizationDao; private final OrganizationMemberDao organizationMemberDao; private final QualityProfileDao qualityProfileDao; + private final QualityProfileExportDao qualityProfileExportDao; private final PropertiesDao propertiesDao; private final AlmAppInstallDao almAppInstallDao; private final ProjectAlmBindingDao projectAlmBindingDao; @@ -167,6 +169,7 @@ public class DbClient { organizationDao = getDao(map, OrganizationDao.class); organizationMemberDao = getDao(map, OrganizationMemberDao.class); qualityProfileDao = getDao(map, QualityProfileDao.class); + qualityProfileExportDao = getDao(map, QualityProfileExportDao.class); propertiesDao = getDao(map, PropertiesDao.class); internalPropertiesDao = getDao(map, InternalPropertiesDao.class); snapshotDao = getDao(map, SnapshotDao.class); @@ -267,6 +270,10 @@ public class DbClient { return qualityProfileDao; } + public QualityProfileExportDao qualityProfileExportDao() { + return qualityProfileExportDao; + } + public PropertiesDao propertiesDao() { return propertiesDao; } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java b/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java index f449a0eeabf..391ee1c0933 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java @@ -48,10 +48,10 @@ import org.sonar.db.ce.CeTaskMessageMapper; import org.sonar.db.component.AnalysisPropertiesMapper; import org.sonar.db.component.BranchMapper; import org.sonar.db.component.ComponentDto; -import org.sonar.db.component.ComponentWithModuleUuidDto; import org.sonar.db.component.ComponentDtoWithSnapshotId; import org.sonar.db.component.ComponentKeyUpdaterMapper; import org.sonar.db.component.ComponentMapper; +import org.sonar.db.component.ComponentWithModuleUuidDto; import org.sonar.db.component.FilePathWithHashDto; import org.sonar.db.component.KeyWithUuidDto; import org.sonar.db.component.ProjectLinkMapper; @@ -121,6 +121,7 @@ import org.sonar.db.qualityprofile.DefaultQProfileMapper; import org.sonar.db.qualityprofile.QProfileChangeMapper; import org.sonar.db.qualityprofile.QProfileEditGroupsMapper; import org.sonar.db.qualityprofile.QProfileEditUsersMapper; +import org.sonar.db.qualityprofile.QualityProfileExportMapper; import org.sonar.db.qualityprofile.QualityProfileMapper; import org.sonar.db.rule.RuleDto; import org.sonar.db.rule.RuleMapper; @@ -270,6 +271,7 @@ public class MyBatis implements Startable { QualityGateConditionMapper.class, QualityGateMapper.class, QualityProfileMapper.class, + QualityProfileExportMapper.class, RoleMapper.class, RuleMapper.class, RuleRepositoryMapper.class, diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ExportRuleDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ExportRuleDto.java new file mode 100644 index 00000000000..9e0c123cef5 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ExportRuleDto.java @@ -0,0 +1,92 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.qualityprofile; + +import java.util.LinkedList; +import java.util.List; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.RuleType; +import org.sonar.db.rule.SeverityUtil; + +public class ExportRuleDto { + private Integer activeRuleId; + private String repository; + private String rule; + private String name; + private String description; + private String extendedDescription; + private String template; + private Integer severity; + private Integer type; + private String tags; + + private List<ExportRuleParamDto> params; + + public boolean isCustomRule() { + return template != null; + } + + public Integer getActiveRuleId() { + return activeRuleId; + } + + public RuleKey getRuleKey() { + return RuleKey.of(repository, rule); + } + + public RuleKey getTemplateRuleKey() { + return RuleKey.of(repository, template); + } + + public String getSeverityString() { + return SeverityUtil.getSeverityFromOrdinal(severity); + } + + public String getExtendedDescription() { + return extendedDescription; + } + + public RuleType getRuleType() { + return RuleType.valueOf(type); + } + + public String getTags() { + return tags; + } + + public String getDescription() { + return description; + } + + public String getName() { + return name; + } + + public List<ExportRuleParamDto> getParams() { + if (params == null) { + params = new LinkedList<>(); + } + return params; + } + + void setParams(List<ExportRuleParamDto> params) { + this.params = params; + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ExportRuleParamDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ExportRuleParamDto.java new file mode 100644 index 00000000000..1222480dd5a --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ExportRuleParamDto.java @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.qualityprofile; + +public class ExportRuleParamDto { + private Integer activeRuleId; + private String kee; + private String value; + + public Integer getActiveRuleId() { + return activeRuleId; + } + + public String getKey() { + return kee; + } + + public String getValue() { + return value; + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QualityProfileExportDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QualityProfileExportDao.java new file mode 100644 index 00000000000..5f2ff75ff8f --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QualityProfileExportDao.java @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.qualityprofile; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.sonar.db.Dao; +import org.sonar.db.DbSession; + +import static org.sonar.db.DatabaseUtils.executeLargeInputs; + +public class QualityProfileExportDao implements Dao { + + public List<ExportRuleDto> selectRulesByProfile(DbSession dbSession, QProfileDto profile) { + List<ExportRuleDto> exportRules = mapper(dbSession).selectByProfileUuid(profile.getKee()); + + Set<Integer> activeRuleIds = exportRules.stream().map(ExportRuleDto::getActiveRuleId).collect(Collectors.toSet()); + + Map<Integer, List<ExportRuleParamDto>> activeRulesParam = selectParamsByActiveRuleIds(dbSession, activeRuleIds); + + exportRules.forEach(exportRuleDto -> + exportRuleDto.setParams(activeRulesParam.get(exportRuleDto.getActiveRuleId())) + ); + return exportRules; + } + + private Map<Integer, List<ExportRuleParamDto>> selectParamsByActiveRuleIds(DbSession dbSession, Collection<Integer> activeRuleIds) { + return executeLargeInputs(activeRuleIds, ids -> mapper(dbSession).selectParamsByActiveRuleIds(ids)) + .stream() + .collect(Collectors.groupingBy(ExportRuleParamDto::getActiveRuleId)); + } + + private static QualityProfileExportMapper mapper(DbSession dbSession) { + return dbSession.getMapper(QualityProfileExportMapper.class); + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QualityProfileExportMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QualityProfileExportMapper.java new file mode 100644 index 00000000000..3a2bb304b07 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QualityProfileExportMapper.java @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.qualityprofile; + +import java.util.Collection; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +public interface QualityProfileExportMapper { + List<ExportRuleDto> selectByProfileUuid(String uuid); + + List<ExportRuleParamDto> selectParamsByActiveRuleIds(@Param("activeRuleIds") Collection<Integer> activeRuleIds); +} diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QualityProfileExportMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QualityProfileExportMapper.xml new file mode 100644 index 00000000000..ec8fe7a81e3 --- /dev/null +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QualityProfileExportMapper.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "mybatis-3-mapper.dtd"> + +<mapper namespace="org.sonar.db.qualityprofile.QualityProfileExportMapper"> + + <sql id="exportRuleColumns"> + a.id as "activeRuleId", + a.failure_level as "severity", + r.plugin_rule_key as "rule", + r.plugin_name as "repository", + r.priority as "defaultSeverity", + r.name, + r.description, + r.description_format as "descriptionFormat", + r.rule_type as "type", + rt.plugin_rule_key as "template", + rm.note_data as "extendedDescription", + rm.tags + </sql> + + <sql id="exportRuleParamColumns"> + p.active_rule_id as activeRuleId, + p.rules_parameter_key as kee, + p.value as value + </sql> + + <select id="selectByProfileUuid" parameterType="string" resultType="org.sonar.db.qualityprofile.ExportRuleDto"> + select + <include refid="exportRuleColumns"/> + from active_rules a + inner join rules_profiles rp on rp.id = a.profile_id + inner join org_qprofiles oqp on oqp.rules_profile_uuid = rp.kee + inner join rules r on r.id = a.rule_id and r.status != 'REMOVED' + left join rules rt on rt.id = r.template_id + left join rules_metadata rm on rm.rule_id = r.id + where oqp.uuid = #{id, jdbcType=VARCHAR} + </select> + + <select id="selectParamsByActiveRuleIds" parameterType="map" resultType="org.sonar.db.qualityprofile.ExportRuleParamDto"> + select + <include refid="exportRuleParamColumns"/> + from active_rule_parameters p + <where> + p.active_rule_id in <foreach collection="activeRuleIds" open="(" close=")" item="activeRuleId" separator=",">#{activeRuleId,jdbcType=INTEGER}</foreach> + </where> + </select> +</mapper> diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/DaoModuleTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/DaoModuleTest.java index 184f56ad80d..cb8424da879 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/DaoModuleTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/DaoModuleTest.java @@ -30,6 +30,6 @@ public class DaoModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new DaoModule().configure(container); - assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 61); + assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 62); } } diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QualityProfileExportDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QualityProfileExportDaoTest.java new file mode 100644 index 00000000000..7b33fb41526 --- /dev/null +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QualityProfileExportDaoTest.java @@ -0,0 +1,227 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.qualityprofile; + +import com.google.common.collect.Sets; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.impl.utils.AlwaysIncreasingSystem2; +import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rules.RuleType; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.db.rule.RuleMetadataDto; +import org.sonar.db.rule.RuleParamDto; + +import static org.assertj.core.api.Assertions.assertThat; + +public class QualityProfileExportDaoTest { + + @Rule + public DbTester db = DbTester.create(AlwaysIncreasingSystem2.INSTANCE); + + private DbSession dbSession = db.getSession(); + private QualityProfileExportDao underTest = db.getDbClient().qualityProfileExportDao(); + + @Test + public void selectRulesByProfile_ready_rules_only() { + String language = "java"; + RuleDefinitionDto rule1 = createRule(language); + RuleDefinitionDto rule2 = createRule(language); + RuleDefinitionDto rule3 = createRule(language); + createRule(language, RuleStatus.REMOVED); + + QProfileDto profile = createProfile(language); + + activate(profile, rule1, rule2, rule3); + + List<ExportRuleDto> results = underTest.selectRulesByProfile(dbSession, profile); + assertThat(results).isNotNull(); + assertThat(results).asList() + .extracting("ruleKey") + .containsOnly(rule1.getKey(), rule2.getKey(), rule3.getKey()); + } + + @Test + public void selectRulesByProfile_verify_columns() { + String language = "java"; + RuleDefinitionDto ruleTemplate = createRule(language); + RuleDefinitionDto rule = createRule(language, RuleStatus.READY, ruleTemplate.getId()); + RuleMetadataDto ruleMetadata = createRuleMetadata(new RuleMetadataDto() + .setRuleId(rule.getId()) + .setOrganizationUuid(db.getDefaultOrganization().getUuid()) + .setNoteData("Extended description") + .setTags(Sets.newHashSet("tag1", "tag2", "tag3"))); + + QProfileDto profile = createProfile(language); + + List<ActiveRuleDto> activeRules = activate(profile, rule); + + List<ExportRuleDto> results = underTest.selectRulesByProfile(dbSession, profile); + assertThat(results).isNotNull(); + assertThat(results).asList().isNotEmpty(); + + ExportRuleDto exportRuleDto = results.get(0); + assertThat(exportRuleDto).isNotNull(); + assertThat(exportRuleDto.getParams()).asList().isEmpty(); + assertThat(exportRuleDto.getDescription()).isEqualTo(rule.getDescription()); + assertThat(exportRuleDto.getExtendedDescription()).isEqualTo(ruleMetadata.getNoteData()); + assertThat(exportRuleDto.getName()).isEqualTo(rule.getName()); + assertThat(exportRuleDto.getRuleKey()).isEqualTo(rule.getKey()); + assertThat(exportRuleDto.getRuleType()).isEqualTo(RuleType.valueOf(rule.getType())); + assertThat(exportRuleDto.getSeverityString()).isEqualTo(activeRules.get(0).getSeverityString()); + assertThat(exportRuleDto.getTags()).isEqualTo(String.join(",", ruleMetadata.getTags())); + assertThat(exportRuleDto.getTemplateRuleKey()).isEqualTo(ruleTemplate.getKey()); + } + + @Test + public void selectRulesByProfile_verify_rows_over_1000() { + String language = "java"; + int numberOfParamsToCreate = 1005; + RuleDefinitionDto rule = createRule(language); + List<RuleParamDto> ruleParams = addParamsToRule(rule, numberOfParamsToCreate); + + QProfileDto profile = createProfile(language); + ActiveRuleDto activatedRule = activate(profile, rule, ruleParams); + + List<ExportRuleDto> results = underTest.selectRulesByProfile(dbSession, profile); + + assertThat(results) + .extracting("activeRuleId") + .containsOnly(activatedRule.getId()); + + assertThat(results.get(0).getParams()) + .extracting("key") + .containsOnly(ruleParams.stream().map(RuleParamDto::getName).toArray()); + } + + @Test + public void selectRulesByProfile_params_assigned_correctly() { + String language = "java"; + RuleDefinitionDto firstRule = createRule(language); + List<RuleParamDto> ruleParamsOfFirstRule = addParamsToRule(firstRule, 2); + + RuleDefinitionDto secondRule = createRule(language); + List<RuleParamDto> ruleParamsOfSecondRule = addParamsToRule(secondRule, 3); + + String otherLanguage = "js"; + RuleDefinitionDto thirdRule = createRule(otherLanguage); + List<RuleParamDto> ruleParamsOfThirdRule = addParamsToRule(thirdRule, 4); + + QProfileDto profile = createProfile(language); + QProfileDto otherProfile = createProfile(otherLanguage); + + ActiveRuleDto firstActivatedRule = activate(profile, firstRule, ruleParamsOfFirstRule); + ActiveRuleDto secondActivatedRule = activate(profile, secondRule, ruleParamsOfSecondRule); + + ActiveRuleDto thirdActivatedRule = activate(otherProfile, thirdRule, ruleParamsOfThirdRule); + + List<ExportRuleDto> results = underTest.selectRulesByProfile(dbSession, profile); + List<ExportRuleDto> otherProfileResults = underTest.selectRulesByProfile(dbSession, otherProfile); + + assertThat(results) + .extracting("activeRuleId") + .containsOnly(firstActivatedRule.getId(), secondActivatedRule.getId()); + + assertThat(otherProfileResults).asList() + .extracting("activeRuleId") + .containsOnly(thirdActivatedRule.getId()); + + ExportRuleDto firstExportedRule = findExportedRuleById(firstActivatedRule.getId(), results); + assertThat(firstExportedRule.getParams()) + .extracting("key") + .containsOnly(ruleParamsOfFirstRule.stream().map(RuleParamDto::getName).toArray()); + + ExportRuleDto secondExportedRule = findExportedRuleById(secondActivatedRule.getId(), results); + assertThat(secondExportedRule.getParams()) + .extracting("key") + .containsOnly(ruleParamsOfSecondRule.stream().map(RuleParamDto::getName).toArray()); + + ExportRuleDto thirdExportedRule = findExportedRuleById(thirdActivatedRule.getId(), otherProfileResults); + assertThat(thirdExportedRule.getParams()) + .extracting("key") + .containsOnly(ruleParamsOfThirdRule.stream().map(RuleParamDto::getName).toArray()); + } + + + private ExportRuleDto findExportedRuleById(Integer id, List<ExportRuleDto> results) { + Optional<ExportRuleDto> found = results.stream().filter(exportRuleDto -> id.equals(exportRuleDto.getActiveRuleId())).findFirst(); + if (!found.isPresent()) { + Assert.fail(); + } + return found.get(); + } + + private List<RuleParamDto> addParamsToRule(RuleDefinitionDto firstRule, int numberOfParams) { + return IntStream.range(0, numberOfParams) + .mapToObj(value -> db.rules().insertRuleParam(firstRule, + ruleParamDto -> ruleParamDto.setName("name_" + firstRule.getId() + "_" + value))) + .collect(Collectors.toList()); + } + + private RuleDefinitionDto createRule(String language) { + return createRule(language, RuleStatus.READY); + } + + private RuleDefinitionDto createRule(String language, RuleStatus status) { + return createRule(language, status, null); + } + + private RuleDefinitionDto createRule(String language, RuleStatus status, @Nullable Integer templateId) { + return db.rules().insert(ruleDefinitionDto -> ruleDefinitionDto.setRepositoryKey("repoKey").setLanguage(language).setStatus(status).setTemplateId(templateId)); + } + + private RuleMetadataDto createRuleMetadata(RuleMetadataDto metadataDto) { + return db.rules().insertOrUpdateMetadata(metadataDto); + } + + private QProfileDto createProfile(String lanugage) { + return db.qualityProfiles().insert(db.getDefaultOrganization(), p -> p.setLanguage(lanugage)); + } + + private List<ActiveRuleDto> activate(QProfileDto profile, RuleDefinitionDto... rules) { + return Stream.of(rules) + .map(ruleDefinitionDto -> db.qualityProfiles().activateRule(profile, ruleDefinitionDto)) + .collect(Collectors.toList()); + } + + private ActiveRuleDto activate(QProfileDto profile, RuleDefinitionDto rule, Collection<RuleParamDto> params) { + ActiveRuleDto activeRule = db.qualityProfiles().activateRule(profile, rule); + + params.forEach(ruleParamDto -> { + ActiveRuleParamDto dto = ActiveRuleParamDto.createFor(ruleParamDto) + .setKey(ruleParamDto.getName()) + .setValue("20") + .setActiveRuleId(activeRule.getId()); + db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRule, dto); + }); + + return activeRule; + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ImportedQProfile.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ImportedQProfile.java new file mode 100644 index 00000000000..be10bdbc503 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ImportedQProfile.java @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.qualityprofile; + +import java.util.List; + +class ImportedQProfile { + private final String profileName; + private final String profileLang; + + private final List<ImportedRule> rules; + + public ImportedQProfile(String profileName, String profileLang, List<ImportedRule> rules) { + this.profileName = profileName; + this.profileLang = profileLang; + this.rules = rules; + } + + public String getProfileName() { + return profileName; + } + + public String getProfileLang() { + return profileLang; + } + + public List<ImportedRule> getRules() { + return rules; + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ImportedRule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ImportedRule.java new file mode 100644 index 00000000000..484bc00c207 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ImportedRule.java @@ -0,0 +1,101 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.qualityprofile; + +import java.util.Map; +import org.sonar.api.rule.RuleKey; + +class ImportedRule { + private RuleKey ruleKey; + private RuleKey templateKey; + private String name; + private String type; + private String severity; + private String description; + + public Map<String, String> getParameters() { + return parameters; + } + + private Map<String, String> parameters; + + public RuleKey getRuleKey() { + return ruleKey; + } + + public RuleKey getTemplateKey() { + return templateKey; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getSeverity() { + return severity; + } + + public String getDescription() { + return description; + } + + ImportedRule setRuleKey(RuleKey ruleKey) { + this.ruleKey = ruleKey; + return this; + } + + ImportedRule setTemplateKey(RuleKey templateKey) { + this.templateKey = templateKey; + return this; + } + + ImportedRule setType(String type) { + this.type = type; + return this; + } + + ImportedRule setSeverity(String severity) { + this.severity = severity; + return this; + } + + ImportedRule setDescription(String description) { + this.description = description; + return this; + } + + ImportedRule setParameters(Map<String, String> parameters) { + this.parameters = parameters; + return this; + } + + ImportedRule setName(String name) { + this.name = name; + return this; + } + + boolean isCustomRule() { + return templateKey != null; + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileBackuperImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileBackuperImpl.java index 48658b6afd0..4a63cba654b 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileBackuperImpl.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileBackuperImpl.java @@ -19,105 +19,56 @@ */ package org.sonar.server.qualityprofile; -import com.google.common.base.Joiner; -import com.google.common.collect.Lists; import java.io.Reader; import java.io.Writer; -import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nullable; -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLStreamException; -import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.builder.CompareToBuilder; -import org.codehaus.staxmate.SMInputFactory; -import org.codehaus.staxmate.in.SMHierarchicCursor; -import org.codehaus.staxmate.in.SMInputCursor; import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rules.RuleType; import org.sonar.api.server.ServerSide; -import org.sonar.api.utils.text.XmlWriter; import org.sonar.core.util.stream.MoreCollectors; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.qualityprofile.ActiveRuleDto; -import org.sonar.db.qualityprofile.ActiveRuleParamDto; -import org.sonar.db.qualityprofile.OrgActiveRuleDto; +import org.sonar.db.qualityprofile.ExportRuleDto; import org.sonar.db.qualityprofile.QProfileDto; import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.server.rule.NewCustomRule; +import org.sonar.server.rule.RuleCreator; import static com.google.common.base.Preconditions.checkArgument; @ServerSide public class QProfileBackuperImpl implements QProfileBackuper { - private static final Joiner RULE_KEY_JOINER = Joiner.on(", ").skipNulls(); - - private static final String ATTRIBUTE_PROFILE = "profile"; - private static final String ATTRIBUTE_NAME = "name"; - private static final String ATTRIBUTE_LANGUAGE = "language"; - - private static final String ATTRIBUTE_RULES = "rules"; - private static final String ATTRIBUTE_RULE = "rule"; - private static final String ATTRIBUTE_REPOSITORY_KEY = "repositoryKey"; - private static final String ATTRIBUTE_KEY = "key"; - private static final String ATTRIBUTE_PRIORITY = "priority"; - - private static final String ATTRIBUTE_PARAMETERS = "parameters"; - private static final String ATTRIBUTE_PARAMETER = "parameter"; - private static final String ATTRIBUTE_PARAMETER_KEY = "key"; - private static final String ATTRIBUTE_PARAMETER_VALUE = "value"; - private final DbClient db; private final QProfileReset profileReset; private final QProfileFactory profileFactory; + private final RuleCreator ruleCreator; + private final QProfileParser qProfileParser; - public QProfileBackuperImpl(DbClient db, QProfileReset profileReset, QProfileFactory profileFactory) { + public QProfileBackuperImpl(DbClient db, QProfileReset profileReset, QProfileFactory profileFactory, + RuleCreator ruleCreator, QProfileParser qProfileParser) { this.db = db; this.profileReset = profileReset; this.profileFactory = profileFactory; + this.ruleCreator = ruleCreator; + this.qProfileParser = qProfileParser; } @Override public void backup(DbSession dbSession, QProfileDto profile, Writer writer) { - List<OrgActiveRuleDto> activeRules = db.activeRuleDao().selectByProfile(dbSession, profile); - activeRules.sort(BackupActiveRuleComparator.INSTANCE); - writeXml(dbSession, writer, profile, activeRules.iterator()); - } - - private void writeXml(DbSession dbSession, Writer writer, QProfileDto profile, Iterator<OrgActiveRuleDto> activeRules) { - XmlWriter xml = XmlWriter.of(writer).declaration(); - xml.begin(ATTRIBUTE_PROFILE); - xml.prop(ATTRIBUTE_NAME, profile.getName()); - xml.prop(ATTRIBUTE_LANGUAGE, profile.getLanguage()); - xml.begin(ATTRIBUTE_RULES); - while (activeRules.hasNext()) { - ActiveRuleDto activeRule = activeRules.next(); - xml.begin(ATTRIBUTE_RULE); - xml.prop(ATTRIBUTE_REPOSITORY_KEY, activeRule.getRuleKey().repository()); - xml.prop(ATTRIBUTE_KEY, activeRule.getRuleKey().rule()); - xml.prop(ATTRIBUTE_PRIORITY, activeRule.getSeverityString()); - xml.begin(ATTRIBUTE_PARAMETERS); - for (ActiveRuleParamDto param : db.activeRuleDao().selectParamsByActiveRuleId(dbSession, activeRule.getId())) { - xml - .begin(ATTRIBUTE_PARAMETER) - .prop(ATTRIBUTE_PARAMETER_KEY, param.getKey()) - .prop(ATTRIBUTE_PARAMETER_VALUE, param.getValue()) - .end(); - } - xml.end(ATTRIBUTE_PARAMETERS); - xml.end(ATTRIBUTE_RULE); - } - xml.end(ATTRIBUTE_RULES).end(ATTRIBUTE_PROFILE).close(); + List<ExportRuleDto> rulesToExport = db.qualityProfileExportDao().selectRulesByProfile(dbSession, profile); + rulesToExport.sort(BackupActiveRuleComparator.INSTANCE); + qProfileParser.writeXml(writer, profile, rulesToExport.iterator()); } @Override @@ -141,49 +92,35 @@ public class QProfileBackuperImpl implements QProfileBackuper { } private QProfileRestoreSummary restore(DbSession dbSession, Reader backup, Function<QProfileName, QProfileDto> profileLoader) { - try { - String profileLang = null; - String profileName = null; - List<Rule> rules = Lists.newArrayList(); - SMInputFactory inputFactory = initStax(); - SMHierarchicCursor rootC = inputFactory.rootElementCursor(backup); - rootC.advance(); // <profile> - if (!ATTRIBUTE_PROFILE.equals(rootC.getLocalName())) { - throw new IllegalArgumentException("Backup XML is not valid. Root element must be <profile>."); - } - SMInputCursor cursor = rootC.childElementCursor(); - while (cursor.getNext() != null) { - String nodeName = cursor.getLocalName(); - if (StringUtils.equals(ATTRIBUTE_NAME, nodeName)) { - profileName = StringUtils.trim(cursor.collectDescendantText(false)); + ImportedQProfile qProfile = qProfileParser.readXml(backup); - } else if (StringUtils.equals(ATTRIBUTE_LANGUAGE, nodeName)) { - profileLang = StringUtils.trim(cursor.collectDescendantText(false)); + QProfileName targetName = new QProfileName(qProfile.getProfileLang(), qProfile.getProfileName()); + QProfileDto targetProfile = profileLoader.apply(targetName); - } else if (StringUtils.equals(ATTRIBUTE_RULES, nodeName)) { - SMInputCursor rulesCursor = cursor.childElementCursor("rule"); - rules = parseRuleActivations(rulesCursor); - } - } + List<ImportedRule> importedRules = qProfile.getRules(); - QProfileName targetName = new QProfileName(profileLang, profileName); - QProfileDto targetProfile = profileLoader.apply(targetName); - List<RuleActivation> ruleActivations = toRuleActivations(dbSession, rules); - BulkChangeResult changes = profileReset.reset(dbSession, targetProfile, ruleActivations); - return new QProfileRestoreSummary(targetProfile, changes); - } catch (XMLStreamException e) { - throw new IllegalArgumentException("Fail to restore Quality profile backup, XML document is not well formed", e); - } + Map<RuleKey, RuleDefinitionDto> ruleDefinitionsByKey = getImportedRulesDefinitions(dbSession, importedRules); + checkIfRulesFromExternalEngines(ruleDefinitionsByKey); + + Map<RuleKey, RuleDefinitionDto> customRulesDefinitions = createCustomRulesIfNotExist(dbSession, importedRules, ruleDefinitionsByKey); + ruleDefinitionsByKey.putAll(customRulesDefinitions); + + List<RuleActivation> ruleActivations = toRuleActivations(importedRules, ruleDefinitionsByKey); + + BulkChangeResult changes = profileReset.reset(dbSession, targetProfile, ruleActivations); + return new QProfileRestoreSummary(targetProfile, changes); } - private List<RuleActivation> toRuleActivations(DbSession dbSession, List<Rule> rules) { + private Map<RuleKey, RuleDefinitionDto> getImportedRulesDefinitions(DbSession dbSession, List<ImportedRule> rules) { List<RuleKey> ruleKeys = rules.stream() - .map(r -> r.ruleKey) + .map(ImportedRule::getRuleKey) .collect(MoreCollectors.toList()); - Map<RuleKey, RuleDefinitionDto> ruleDefinitionsByKey = db.ruleDao().selectDefinitionByKeys(dbSession, ruleKeys) + return db.ruleDao().selectDefinitionByKeys(dbSession, ruleKeys) .stream() - .collect(MoreCollectors.uniqueIndex(RuleDefinitionDto::getKey)); + .collect(Collectors.toMap(RuleDefinitionDto::getKey, Function.identity())); + } + private void checkIfRulesFromExternalEngines(Map<RuleKey, RuleDefinitionDto> ruleDefinitionsByKey) { List<RuleDefinitionDto> externalRules = ruleDefinitionsByKey.values().stream() .filter(RuleDefinitionDto::isExternal) .collect(Collectors.toList()); @@ -192,105 +129,51 @@ public class QProfileBackuperImpl implements QProfileBackuper { throw new IllegalArgumentException("The quality profile cannot be restored as it contains rules from external rule engines: " + externalRules.stream().map(r -> r.getKey().toString()).collect(Collectors.joining(", "))); } - - return rules.stream() - .map(r -> { - RuleDefinitionDto ruleDefinition = ruleDefinitionsByKey.get(r.ruleKey); - if (ruleDefinition == null) { - return null; - } - return RuleActivation.create(ruleDefinition.getId(), r.severity, r.parameters); - }) - .filter(Objects::nonNull) - .collect(MoreCollectors.toList(rules.size())); } - private static final class Rule { - private final RuleKey ruleKey; - private final String severity; - private final Map<String, String> parameters; + private Map<RuleKey, RuleDefinitionDto> createCustomRulesIfNotExist(DbSession dbSession, List<ImportedRule> rules, Map<RuleKey, RuleDefinitionDto> ruleDefinitionsByKey) { + List<NewCustomRule> customRulesToCreate = rules.stream() + .filter(r -> ruleDefinitionsByKey.get(r.getRuleKey()) == null && r.isCustomRule()) + .map(QProfileBackuperImpl::importedRuleToNewCustomRule) + .collect(Collectors.toList()); - private Rule(RuleKey ruleKey, String severity, Map<String, String> parameters) { - this.ruleKey = ruleKey; - this.severity = severity; - this.parameters = parameters; + if (!customRulesToCreate.isEmpty()) { + return db.ruleDao().selectDefinitionByKeys(dbSession, ruleCreator.create(dbSession, customRulesToCreate)) + .stream() + .collect(Collectors.toMap(RuleDefinitionDto::getKey, Function.identity())); } + return Collections.emptyMap(); } - private static List<Rule> parseRuleActivations(SMInputCursor rulesCursor) throws XMLStreamException { - List<Rule> activations = new ArrayList<>(); - Set<RuleKey> activatedKeys = new HashSet<>(); - List<RuleKey> duplicatedKeys = new ArrayList<>(); - while (rulesCursor.getNext() != null) { - SMInputCursor ruleCursor = rulesCursor.childElementCursor(); - String repositoryKey = null; - String key = null; - String severity = null; - Map<String, String> parameters = new HashMap<>(); - while (ruleCursor.getNext() != null) { - String nodeName = ruleCursor.getLocalName(); - if (StringUtils.equals(ATTRIBUTE_REPOSITORY_KEY, nodeName)) { - repositoryKey = StringUtils.trim(ruleCursor.collectDescendantText(false)); - - } else if (StringUtils.equals(ATTRIBUTE_KEY, nodeName)) { - key = StringUtils.trim(ruleCursor.collectDescendantText(false)); - - } else if (StringUtils.equals(ATTRIBUTE_PRIORITY, nodeName)) { - severity = StringUtils.trim(ruleCursor.collectDescendantText(false)); - - } else if (StringUtils.equals(ATTRIBUTE_PARAMETERS, nodeName)) { - SMInputCursor propsCursor = ruleCursor.childElementCursor(ATTRIBUTE_PARAMETER); - readParameters(propsCursor, parameters); - } - } - RuleKey ruleKey = RuleKey.of(repositoryKey, key); - if (activatedKeys.contains(ruleKey)) { - duplicatedKeys.add(ruleKey); - } - activatedKeys.add(ruleKey); - activations.add(new Rule(ruleKey, severity, parameters)); - } - if (!duplicatedKeys.isEmpty()) { - throw new IllegalArgumentException("The quality profile cannot be restored as it contains duplicates for the following rules: " + - RULE_KEY_JOINER.join(duplicatedKeys)); - } - return activations; + private static NewCustomRule importedRuleToNewCustomRule(ImportedRule r) { + return NewCustomRule.createForCustomRule(r.getRuleKey().rule(), r.getTemplateKey()) + .setName(r.getName()) + .setMarkdownDescription(r.getDescription()) + .setSeverity(r.getSeverity()) + .setStatus(RuleStatus.READY) + .setPreventReactivation(true) + .setType(RuleType.valueOf(r.getType())) + .setParameters(r.getParameters()); } - private static void readParameters(SMInputCursor propsCursor, Map<String, String> parameters) throws XMLStreamException { - while (propsCursor.getNext() != null) { - SMInputCursor propCursor = propsCursor.childElementCursor(); - String key = null; - String value = null; - while (propCursor.getNext() != null) { - String nodeName = propCursor.getLocalName(); - if (StringUtils.equals(ATTRIBUTE_PARAMETER_KEY, nodeName)) { - key = StringUtils.trim(propCursor.collectDescendantText(false)); - } else if (StringUtils.equals(ATTRIBUTE_PARAMETER_VALUE, nodeName)) { - value = StringUtils.trim(propCursor.collectDescendantText(false)); + private List<RuleActivation> toRuleActivations(List<ImportedRule> rules, Map<RuleKey, RuleDefinitionDto> ruleDefinitionsByKey) { + return rules.stream() + .map(r -> { + RuleDefinitionDto ruleDefinition = ruleDefinitionsByKey.get(r.getRuleKey()); + if (ruleDefinition == null) { + return null; } - } - if (key != null) { - parameters.put(key, value); - } - } - } - - private static SMInputFactory initStax() { - XMLInputFactory xmlFactory = XMLInputFactory.newInstance(); - xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); - xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE); - // just so it won't try to load DTD in if there's DOCTYPE - xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE); - xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE); - return new SMInputFactory(xmlFactory); + return RuleActivation.create(ruleDefinition.getId(), r.getSeverity(), r.getParameters()); + }) + .filter(Objects::nonNull) + .collect(MoreCollectors.toList(rules.size())); } - private enum BackupActiveRuleComparator implements Comparator<ActiveRuleDto> { + private enum BackupActiveRuleComparator implements Comparator<ExportRuleDto> { INSTANCE; @Override - public int compare(ActiveRuleDto o1, ActiveRuleDto o2) { + public int compare(ExportRuleDto o1, ExportRuleDto o2) { return new CompareToBuilder() .append(o1.getRuleKey().repository(), o2.getRuleKey().repository()) .append(o1.getRuleKey().rule(), o2.getRuleKey().rule()) diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileParser.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileParser.java new file mode 100644 index 00000000000..fad9bd08447 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileParser.java @@ -0,0 +1,216 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.qualityprofile; + +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; +import java.io.Reader; +import java.io.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import org.apache.commons.lang.StringUtils; +import org.codehaus.staxmate.SMInputFactory; +import org.codehaus.staxmate.in.SMHierarchicCursor; +import org.codehaus.staxmate.in.SMInputCursor; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.text.XmlWriter; +import org.sonar.db.qualityprofile.ExportRuleDto; +import org.sonar.db.qualityprofile.ExportRuleParamDto; +import org.sonar.db.qualityprofile.QProfileDto; + +@ServerSide +public class QProfileParser { + private static final Joiner RULE_KEY_JOINER = Joiner.on(", ").skipNulls(); + + private static final String ATTRIBUTE_PROFILE = "profile"; + private static final String ATTRIBUTE_NAME = "name"; + private static final String ATTRIBUTE_LANGUAGE = "language"; + + private static final String ATTRIBUTE_RULES = "rules"; + private static final String ATTRIBUTE_RULE = "rule"; + private static final String ATTRIBUTE_REPOSITORY_KEY = "repositoryKey"; + private static final String ATTRIBUTE_KEY = "key"; + private static final String ATTRIBUTE_PRIORITY = "priority"; + private static final String ATTRIBUTE_TEMPLATE_KEY = "templateKey"; + private static final String ATTRIBUTE_TYPE = "type"; + private static final String ATTRIBUTE_DESCRIPTION = "description"; + + private static final String ATTRIBUTE_PARAMETERS = "parameters"; + private static final String ATTRIBUTE_PARAMETER = "parameter"; + private static final String ATTRIBUTE_PARAMETER_KEY = "key"; + private static final String ATTRIBUTE_PARAMETER_VALUE = "value"; + + public QProfileParser() { + } + + public void writeXml(Writer writer, QProfileDto profile, Iterator<ExportRuleDto> rulesToExport) { + XmlWriter xml = XmlWriter.of(writer).declaration(); + xml.begin(ATTRIBUTE_PROFILE); + xml.prop(ATTRIBUTE_NAME, profile.getName()); + xml.prop(ATTRIBUTE_LANGUAGE, profile.getLanguage()); + xml.begin(ATTRIBUTE_RULES); + while (rulesToExport.hasNext()) { + ExportRuleDto ruleToExport = rulesToExport.next(); + xml.begin(ATTRIBUTE_RULE); + xml.prop(ATTRIBUTE_REPOSITORY_KEY, ruleToExport.getRuleKey().repository()); + xml.prop(ATTRIBUTE_KEY, ruleToExport.getRuleKey().rule()); + xml.prop(ATTRIBUTE_TYPE, ruleToExport.getRuleType().name()); + xml.prop(ATTRIBUTE_PRIORITY, ruleToExport.getSeverityString()); + + if (ruleToExport.isCustomRule()) { + xml.prop(ATTRIBUTE_NAME, ruleToExport.getName()); + xml.prop(ATTRIBUTE_TEMPLATE_KEY, ruleToExport.getTemplateRuleKey().rule()); + xml.prop(ATTRIBUTE_DESCRIPTION, ruleToExport.getDescription()); + } + + xml.begin(ATTRIBUTE_PARAMETERS); + for (ExportRuleParamDto param : ruleToExport.getParams()) { + xml + .begin(ATTRIBUTE_PARAMETER) + .prop(ATTRIBUTE_PARAMETER_KEY, param.getKey()) + .prop(ATTRIBUTE_PARAMETER_VALUE, param.getValue()) + .end(); + } + xml.end(ATTRIBUTE_PARAMETERS); + xml.end(ATTRIBUTE_RULE); + } + xml.end(ATTRIBUTE_RULES).end(ATTRIBUTE_PROFILE).close(); + } + + public ImportedQProfile readXml(Reader reader) { + List<ImportedRule> rules = Lists.newArrayList(); + String profileName = null; + String profileLang = null; + try { + SMInputFactory inputFactory = initStax(); + SMHierarchicCursor rootC = inputFactory.rootElementCursor(reader); + rootC.advance(); // <profile> + if (!ATTRIBUTE_PROFILE.equals(rootC.getLocalName())) { + throw new IllegalArgumentException("Backup XML is not valid. Root element must be <profile>."); + } + SMInputCursor cursor = rootC.childElementCursor(); + + while (cursor.getNext() != null) { + String nodeName = cursor.getLocalName(); + if (StringUtils.equals(ATTRIBUTE_NAME, nodeName)) { + profileName = StringUtils.trim(cursor.collectDescendantText(false)); + } else if (StringUtils.equals(ATTRIBUTE_LANGUAGE, nodeName)) { + profileLang = StringUtils.trim(cursor.collectDescendantText(false)); + } else if (StringUtils.equals(ATTRIBUTE_RULES, nodeName)) { + SMInputCursor rulesCursor = cursor.childElementCursor("rule"); + rules = parseRuleActivations(rulesCursor); + } + } + } catch (XMLStreamException e) { + throw new IllegalArgumentException("Fail to restore Quality profile backup, XML document is not well formed", e); + } + return new ImportedQProfile(profileName, profileLang, rules); + } + + private static SMInputFactory initStax() { + XMLInputFactory xmlFactory = XMLInputFactory.newInstance(); + xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); + xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE); + // just so it won't try to load DTD in if there's DOCTYPE + xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE); + xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE); + return new SMInputFactory(xmlFactory); + } + + private static List<ImportedRule> parseRuleActivations(SMInputCursor rulesCursor) throws XMLStreamException { + List<ImportedRule> activations = new ArrayList<>(); + Set<RuleKey> activatedKeys = new HashSet<>(); + List<RuleKey> duplicatedKeys = new ArrayList<>(); + while (rulesCursor.getNext() != null) { + SMInputCursor ruleCursor = rulesCursor.childElementCursor(); + Map<String, String> parameters = new HashMap<>(); + String repositoryKey = null; + String key = null; + String templateKey = null; + ImportedRule rule = new ImportedRule(); + while (ruleCursor.getNext() != null) { + String nodeName = ruleCursor.getLocalName(); + if (StringUtils.equals(ATTRIBUTE_REPOSITORY_KEY, nodeName)) { + repositoryKey = StringUtils.trim(ruleCursor.collectDescendantText(false)); + } else if (StringUtils.equals(ATTRIBUTE_KEY, nodeName)) { + key = StringUtils.trim(ruleCursor.collectDescendantText(false)); + } else if (StringUtils.equals(ATTRIBUTE_TEMPLATE_KEY, nodeName)) { + templateKey = StringUtils.trim(ruleCursor.collectDescendantText(false)); + } else if (StringUtils.equals(ATTRIBUTE_NAME, nodeName)) { + rule.setName(StringUtils.trim(ruleCursor.collectDescendantText(false))); + } else if (StringUtils.equals(ATTRIBUTE_TYPE, nodeName)) { + rule.setType(StringUtils.trim(ruleCursor.collectDescendantText(false))); + } else if (StringUtils.equals(ATTRIBUTE_DESCRIPTION, nodeName)) { + rule.setDescription(StringUtils.trim(ruleCursor.collectDescendantText(false))); + } else if (StringUtils.equals(ATTRIBUTE_PRIORITY, nodeName)) { + rule.setSeverity(StringUtils.trim(ruleCursor.collectDescendantText(false))); + } else if (StringUtils.equals(ATTRIBUTE_PARAMETERS, nodeName)) { + SMInputCursor propsCursor = ruleCursor.childElementCursor(ATTRIBUTE_PARAMETER); + readParameters(propsCursor, parameters); + rule.setParameters(parameters); + } + } + RuleKey ruleKey = RuleKey.of(repositoryKey, key); + rule.setRuleKey(ruleKey); + + if (templateKey != null) { + rule.setTemplateKey(RuleKey.of(repositoryKey, templateKey)); + } + + if (activatedKeys.contains(ruleKey)) { + duplicatedKeys.add(ruleKey); + } + activatedKeys.add(ruleKey); + activations.add(rule); + } + if (!duplicatedKeys.isEmpty()) { + throw new IllegalArgumentException("The quality profile cannot be restored as it contains duplicates for the following rules: " + + RULE_KEY_JOINER.join(duplicatedKeys)); + } + return activations; + } + + private static void readParameters(SMInputCursor propsCursor, Map<String, String> parameters) throws XMLStreamException { + while (propsCursor.getNext() != null) { + SMInputCursor propCursor = propsCursor.childElementCursor(); + String key = null; + String value = null; + while (propCursor.getNext() != null) { + String nodeName = propCursor.getLocalName(); + if (StringUtils.equals(ATTRIBUTE_PARAMETER_KEY, nodeName)) { + key = StringUtils.trim(propCursor.collectDescendantText(false)); + } else if (StringUtils.equals(ATTRIBUTE_PARAMETER_VALUE, nodeName)) { + value = StringUtils.trim(propCursor.collectDescendantText(false)); + } + } + if (key != null) { + parameters.put(key, value); + } + } + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/RuleCreator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/RuleCreator.java index 5a4074a20bd..e841491dde7 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/RuleCreator.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/RuleCreator.java @@ -23,8 +23,11 @@ import com.google.common.base.Splitter; import com.google.common.base.Strings; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.rule.RuleKey; @@ -89,6 +92,46 @@ public class RuleCreator { return customRuleKey; } + public List<RuleKey> create(DbSession dbSession, List<NewCustomRule> newRules) { + String defaultOrganizationUuid = defaultOrganizationProvider.get().getUuid(); + OrganizationDto defaultOrganization = dbClient.organizationDao().selectByUuid(dbSession, defaultOrganizationUuid) + .orElseThrow(() -> new IllegalStateException(format("Could not find default organization for uuid '%s'", defaultOrganizationUuid))); + + Set<RuleKey> templateKeys = newRules.stream().map(NewCustomRule::templateKey).collect(Collectors.toSet()); + Map<RuleKey, RuleDto> templateRules = dbClient.ruleDao().selectByKeys(dbSession, defaultOrganization.getUuid(), templateKeys) + .stream() + .collect(Collectors.toMap( + RuleDto::getKey, + Function.identity()) + ); + + checkArgument(!templateRules.isEmpty() && templateKeys.size() == templateRules.size(), "Rule template keys should exists for each custom rule!"); + templateRules.values().forEach(ruleDto -> { + checkArgument(ruleDto.isTemplate(), "This rule is not a template rule: %s", ruleDto.getKey().toString()); + checkArgument(ruleDto.getStatus() != RuleStatus.REMOVED, "The template key doesn't exist: %s", ruleDto.getKey().toString()); + } + ); + + List<Integer> customRuleIds = newRules.stream() + .map(newCustomRule -> { + RuleDto templateRule = templateRules.get(newCustomRule.templateKey()); + validateCustomRule(newCustomRule, dbSession, templateRule.getKey()); + RuleKey customRuleKey = RuleKey.of(templateRule.getRepositoryKey(), newCustomRule.ruleKey()); + return createCustomRule(customRuleKey, newCustomRule, templateRule, dbSession); + } + ) + .collect(Collectors.toList()); + + ruleIndexer.commitAndIndex(dbSession, customRuleIds); + return newRules.stream() + .map(newCustomRule -> { + RuleDto templateRule = templateRules.get(newCustomRule.templateKey()); + return RuleKey.of(templateRule.getRepositoryKey(), newCustomRule.ruleKey()); + } + ) + .collect(Collectors.toList()); + } + private void validateCustomRule(NewCustomRule newRule, DbSession dbSession, RuleKey templateKey) { List<String> errors = new ArrayList<>(); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileBackuperImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileBackuperImplTest.java index 9e27015bc5f..942de489af8 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileBackuperImplTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileBackuperImplTest.java @@ -25,12 +25,15 @@ import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.sonar.api.impl.utils.AlwaysIncreasingSystem2; import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rules.RuleType; import org.sonar.api.utils.System2; import org.sonar.db.DbSession; import org.sonar.db.DbTester; @@ -40,12 +43,18 @@ import org.sonar.db.qualityprofile.ActiveRuleParamDto; import org.sonar.db.qualityprofile.QProfileDto; import org.sonar.db.qualityprofile.QualityProfileTesting; import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.db.rule.RuleMetadataDto; import org.sonar.db.rule.RuleParamDto; +import org.sonar.server.rule.RuleCreator; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.junit.rules.ExpectedException.none; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class QProfileBackuperImplTest { @@ -64,7 +73,9 @@ public class QProfileBackuperImplTest { private DummyReset reset = new DummyReset(); private QProfileFactory profileFactory = new DummyProfileFactory(); - private QProfileBackuper underTest = new QProfileBackuperImpl(db.getDbClient(), reset, profileFactory); + private RuleCreator ruleCreator = mock(RuleCreator.class); + + private QProfileBackuper underTest = new QProfileBackuperImpl(db.getDbClient(), reset, profileFactory, ruleCreator, new QProfileParser()); @Test public void backup_generates_xml_file() { @@ -82,6 +93,7 @@ public class QProfileBackuperImplTest { "<rule>" + "<repositoryKey>" + rule.getRepositoryKey() + "</repositoryKey>" + "<key>" + rule.getRuleKey() + "</key>" + + "<type>" + RuleType.valueOf(rule.getType()).name() + "</type>" + "<priority>" + activeRule.getSeverityString() + "</priority>" + "<parameters/>" + "</rule>" + @@ -103,6 +115,7 @@ public class QProfileBackuperImplTest { "<rule>" + "<repositoryKey>" + rule.getRepositoryKey() + "</repositoryKey>" + "<key>" + rule.getRuleKey() + "</key>" + + "<type>" + RuleType.valueOf(rule.getType()).name() + "</type>" + "<priority>" + activeRule.getSeverityString() + "</priority>" + "<parameters><parameter>" + "<key>" + param.getName() + "</key>" + @@ -127,6 +140,41 @@ public class QProfileBackuperImplTest { } @Test + public void backup_custom_rules_with_params() { + RuleDefinitionDto templateRule = db.rules().insert(ruleDefinitionDto -> ruleDefinitionDto + .setIsTemplate(true)); + RuleDefinitionDto rule = db.rules().insert(ruleDefinitionDto -> ruleDefinitionDto + .setDescription("custom rule description") + .setName("custom rule name") + .setStatus(RuleStatus.READY) + .setTemplateId(templateRule.getId())); + RuleParamDto param = db.rules().insertRuleParam(rule); + QProfileDto profile = createProfile(rule); + ActiveRuleDto activeRule = activate(profile, rule, param); + + StringWriter writer = new StringWriter(); + underTest.backup(db.getSession(), profile, writer); + + assertThat(writer.toString()).isEqualTo("<?xml version='1.0' encoding='UTF-8'?>" + + "<profile>" + + "<name>" + profile.getName() + "</name>" + + "<language>" + profile.getLanguage() + "</language>" + + "<rules><rule>" + + "<repositoryKey>" + rule.getRepositoryKey() + "</repositoryKey>" + + "<key>" + rule.getKey().rule() + "</key>" + + "<type>" + RuleType.valueOf(rule.getType()) + "</type>" + + "<priority>" + activeRule.getSeverityString() + "</priority>" + + "<name>" + rule.getName() + "</name>" + + "<templateKey>" + templateRule.getKey().rule() + "</templateKey>" + + "<description>" + rule.getDescription() + "</description>" + + "<parameters><parameter>" + + "<key>" + param.getName() + "</key>" + + "<value>20</value>" + + "</parameter></parameters>" + + "</rule></rules></profile>"); + } + + @Test public void restore_backup_on_the_profile_specified_in_backup() { OrganizationDto organization = db.organizations().insert(); Reader backup = new StringReader(EMPTY_BACKUP); @@ -183,6 +231,72 @@ public class QProfileBackuperImplTest { } @Test + public void restore_custom_rule() { + when(ruleCreator.create(any(), anyList())).then(invocation -> + Collections.singletonList(db.rules().insert(RuleKey.of("sonarjs", "s001")).getKey())); + + OrganizationDto organization = db.organizations().insert(); + Reader backup = new StringReader("<?xml version='1.0' encoding='UTF-8'?>" + + "<profile>" + + "<name>custom rule</name>" + + "<language>js</language>" + + "<rules><rule>" + + "<repositoryKey>sonarjs</repositoryKey>" + + "<key>s001</key>" + + "<type>CODE_SMELL</type>" + + "<priority>CRITICAL</priority>" + + "<name>custom rule name</name>" + + "<templateKey>rule_mc8</templateKey>" + + "<description>custom rule description</description>" + + "<parameters><parameter>" + + "<key>bar</key>" + + "<value>baz</value>" + + "</parameter>" + + "</parameters>" + + "</rule></rules></profile>"); + + underTest.restore(db.getSession(), backup, organization, null); + + assertThat(reset.calledActivations).hasSize(1); + RuleActivation activation = reset.calledActivations.get(0); + assertThat(activation.getSeverity()).isEqualTo("CRITICAL"); + assertThat(activation.getParameter("bar")).isEqualTo("baz"); + } + + @Test + public void restore_skips_rule_without_template_key_and_db_definition() { + Integer ruleId = db.rules().insert(RuleKey.of("sonarjs", "s001")).getId(); + OrganizationDto organization = db.organizations().insert(); + Reader backup = new StringReader("<?xml version='1.0' encoding='UTF-8'?>" + + "<profile><name>foo</name>" + + "<language>js</language>" + + "<rules>" + + "<rule>" + + "<repositoryKey>sonarjs</repositoryKey>" + + "<key>s001</key>" + + "<priority>BLOCKER</priority>" + + "<parameters>" + + "<parameter><key>bar</key><value>baz</value></parameter>" + + "</parameters>" + + "</rule>" + + "<rule>" + + "<repositoryKey>sonarjs</repositoryKey>" + + "<key>s002</key>" + + "<priority>MAJOR</priority>" + + "</rule>" + + "</rules>" + + "</profile>"); + + underTest.restore(db.getSession(), backup, organization, null); + + assertThat(reset.calledActivations).hasSize(1); + RuleActivation activation = reset.calledActivations.get(0); + assertThat(activation.getRuleId()).isEqualTo(ruleId); + assertThat(activation.getSeverity()).isEqualTo("BLOCKER"); + assertThat(activation.getParameter("bar")).isEqualTo("baz"); + } + + @Test public void fail_to_restore_if_bad_xml_format() { OrganizationDto organization = db.organizations().insert(); try { @@ -256,6 +370,10 @@ public class QProfileBackuperImplTest { return db.rules().insert(); } + private RuleMetadataDto createRuleMetadata(RuleMetadataDto metadataDto) { + return db.rules().insertOrUpdateMetadata(metadataDto); + } + private QProfileDto createProfile(RuleDefinitionDto rule) { return db.qualityProfiles().insert(db.getDefaultOrganization(), p -> p.setLanguage(rule.getLanguage())); } @@ -284,6 +402,7 @@ public class QProfileBackuperImplTest { return new BulkChangeResult(); } } + private static class DummyProfileFactory implements QProfileFactory { @Override public QProfileDto getOrCreateCustom(DbSession dbSession, OrganizationDto organization, QProfileName key) { diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/BackupActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/BackupActionTest.java index 19630f1fd14..1d9cd581d36 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/BackupActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/BackupActionTest.java @@ -36,6 +36,7 @@ import org.sonar.server.organization.DefaultOrganizationProvider; import org.sonar.server.organization.TestDefaultOrganizationProvider; import org.sonar.server.qualityprofile.QProfileBackuper; import org.sonar.server.qualityprofile.QProfileBackuperImpl; +import org.sonar.server.qualityprofile.QProfileParser; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestResponse; import org.sonar.server.ws.WsActionTester; @@ -57,7 +58,7 @@ public class BackupActionTest { @Rule public UserSessionRule userSession = UserSessionRule.standalone(); - private QProfileBackuper backuper = new QProfileBackuperImpl(db.getDbClient(), null, null); + private QProfileBackuper backuper = new QProfileBackuperImpl(db.getDbClient(), null, null, null, new QProfileParser()); private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db); private QProfileWsSupport wsSupport = new QProfileWsSupport(db.getDbClient(), userSession, defaultOrganizationProvider); private Languages languages = LanguageTesting.newLanguages(A_LANGUAGE); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleCreatorTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleCreatorTest.java index 45bef460214..dca3d48b90e 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleCreatorTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleCreatorTest.java @@ -21,6 +21,8 @@ package org.sonar.server.rule; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; +import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.List; import org.assertj.core.api.Fail; @@ -187,6 +189,53 @@ public class RuleCreatorTest { } @Test + public void batch_create_custom_rules() { + // insert template rule + RuleDefinitionDto templateRule = createTemplateRuleWithIntArrayParam(); + + NewCustomRule firstRule = NewCustomRule.createForCustomRule("CUSTOM_RULE_1", templateRule.getKey()) + .setName("My custom") + .setHtmlDescription("Some description") + .setSeverity(Severity.MAJOR) + .setStatus(RuleStatus.READY); + + NewCustomRule secondRule = NewCustomRule.createForCustomRule("CUSTOM_RULE_2", templateRule.getKey()) + .setName("My custom") + .setHtmlDescription("Some description") + .setSeverity(Severity.MAJOR) + .setStatus(RuleStatus.READY); + + List<RuleKey> customRuleKeys = underTest.create(dbSession, Arrays.asList(firstRule, secondRule)); + + List<RuleDto> rules = dbTester.getDbClient().ruleDao().selectByKeys(dbSession, dbTester.getDefaultOrganization(), customRuleKeys); + + assertThat(rules).hasSize(2); + assertThat(rules).asList() + .extracting("ruleKey") + .containsOnly("CUSTOM_RULE_1", "CUSTOM_RULE_2"); + } + + @Test + public void fail_to_create_custom_rules_when_wrong_rule_template() { + // insert rule + RuleDefinitionDto rule = newRule(RuleKey.of("java", "S001")).setIsTemplate(false); + dbTester.rules().insert(rule); + dbSession.commit(); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("This rule is not a template rule: java:S001"); + + // Create custom rule with unknown template rule + NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", rule.getKey()) + .setName("My custom") + .setHtmlDescription("Some description") + .setSeverity(Severity.MAJOR) + .setStatus(RuleStatus.READY) + .setParameters(ImmutableMap.of("regex", "a.*")); + underTest.create(dbSession, Collections.singletonList(newRule)); + } + + @Test public void fail_to_create_custom_rule_with_invalid_parameter() { // insert template rule RuleDefinitionDto templateRule = createTemplateRuleWithIntArrayParam(); diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index c9d39d14d9d..c00cf3852c9 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -115,7 +115,7 @@ import org.sonar.server.permission.ws.PermissionsWsModule; import org.sonar.server.platform.BackendCleanup; import org.sonar.server.platform.ClusterVerification; import org.sonar.server.platform.PersistentSettings; -import org.sonar.server.setting.SettingsChangeNotifier; +import org.sonar.server.platform.SystemInfoWriterModule; import org.sonar.server.platform.WebCoreExtensionsInstaller; import org.sonar.server.platform.web.DeprecatedPropertiesWsFilter; import org.sonar.server.platform.web.WebServiceFilter; @@ -126,7 +126,6 @@ import org.sonar.server.platform.ws.HealthCheckerModule; import org.sonar.server.platform.ws.L10nWs; import org.sonar.server.platform.ws.ServerWs; import org.sonar.server.platform.ws.SystemWsModule; -import org.sonar.server.platform.SystemInfoWriterModule; import org.sonar.server.plugins.PluginDownloader; import org.sonar.server.plugins.PluginUninstaller; import org.sonar.server.plugins.ServerExtensionInstaller; @@ -159,6 +158,7 @@ import org.sonar.server.qualityprofile.QProfileComparison; import org.sonar.server.qualityprofile.QProfileCopier; import org.sonar.server.qualityprofile.QProfileExporters; import org.sonar.server.qualityprofile.QProfileFactoryImpl; +import org.sonar.server.qualityprofile.QProfileParser; import org.sonar.server.qualityprofile.QProfileResetImpl; import org.sonar.server.qualityprofile.QProfileRulesImpl; import org.sonar.server.qualityprofile.QProfileTreeImpl; @@ -181,8 +181,9 @@ import org.sonar.server.rule.ws.RuleQueryFactory; import org.sonar.server.rule.ws.RuleWsSupport; import org.sonar.server.rule.ws.RulesWs; import org.sonar.server.rule.ws.TagsAction; -import org.sonar.server.setting.ws.SettingsWsModule; import org.sonar.server.setting.ProjectConfigurationLoaderImpl; +import org.sonar.server.setting.SettingsChangeNotifier; +import org.sonar.server.setting.ws.SettingsWsModule; import org.sonar.server.source.ws.SourceWsModule; import org.sonar.server.startup.LogServerId; import org.sonar.server.telemetry.TelemetryClient; @@ -286,6 +287,7 @@ public class PlatformLevel4 extends PlatformLevel { QProfileFactoryImpl.class, QProfileCopier.class, QProfileBackuperImpl.class, + QProfileParser.class, QProfileResetImpl.class, QProfilesWsModule.class, |