]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9304 share built-in profiles among organizations
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Sun, 28 May 2017 13:34:04 +0000 (15:34 +0200)
committerEric Hartmann <hartmann.eric@gmail.com>
Wed, 14 Jun 2017 13:43:12 +0000 (15:43 +0200)
30 files changed:
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java
server/sonar-db-core/src/main/resources/org/sonar/db/version/schema-h2.ddl
server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java
server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java
server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java
server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/DefaultQProfileDao.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/DefaultQProfileDto.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/DefaultQProfileMapper.java [new file with mode: 0644]
server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/DefaultQProfileMapper.xml [new file with mode: 0644]
server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileChangeMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/DaoModuleTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/ActiveRuleParamDtoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/DefaultQProfileDaoTest.java [new file with mode: 0644]
server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QualityProfileDbTester.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v65/CreateTableDefaultQProfiles.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v65/DbVersion65.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v65/PopulateTableDefaultQProfiles.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v65/CreateTableDefaultQProfilesTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v65/DbVersion65Test.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v65/PopulateTableDefaultQProfilesTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v65/CreateTableDefaultQProfilesTest/empty.sql [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v65/PopulateTableDefaultQProfilesTest/initial.sql [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/BuiltInQProfileInsertImpl.java
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileFactory.java
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/CopyAction.java
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/DeleteAction.java
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SetDefaultAction.java
server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/CopyActionTest.java
server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/DeleteActionTest.java

index 0ddfbc9488826446e5331849c4dc12ad5bc3eee0..c55c3f304171f188499355c9b03c12f4762f8f98 100644 (file)
@@ -135,7 +135,7 @@ public class ComputeEngineContainerImplTest {
     assertThat(picoContainer.getParent().getParent().getParent().getComponentAdapters()).hasSize(
       COMPONENTS_IN_LEVEL_1_AT_CONSTRUCTION
         + 23 // level 1
-        + 45 // content of DaoModule
+        + 46 // content of DaoModule
         + 3 // content of EsSearchModule
         + 56 // content of CorePropertyDefinitions
     );
index b3be68dc6c62cc29883f763585f76194a424a1d8..0a6eadea9f57b2358de1fe9d2b7958bf6e307ec8 100644 (file)
@@ -52,6 +52,7 @@ public final class SqTables {
     "ce_queue",
     "ce_task_input",
     "ce_scanner_context",
+    "default_qprofiles",
     "duplications_index",
     "events",
     "file_sources",
index c23374120817f9d40bd95a83f4d78316612724b6..1a2ddb355f79fa3e7e9fae6f6be33d6ff24c060c 100644 (file)
@@ -61,6 +61,17 @@ CREATE TABLE "RULES_PROFILES" (
 CREATE UNIQUE INDEX "UNIQ_QPROF_KEY" ON "RULES_PROFILES" ("KEE");
 
 
+CREATE TABLE "DEFAULT_QPROFILES" (
+  "ORGANIZATION_UUID" VARCHAR(40) NOT NULL,
+  "LANGUAGE" VARCHAR(20) NOT NULL,
+  "QPROFILE_UUID" VARCHAR(40) NOT NULL,
+  "CREATED_AT" BIGINT NOT NULL,
+  "UPDATED_AT" BIGINT NOT NULL
+);
+CREATE PRIMARY KEY ON "DEFAULT_QPROFILES" ("ORGANIZATION_UUID", "LANGUAGE");
+CREATE UNIQUE INDEX "UNIQ_DEFAULT_QPROFILES_UUID" ON "DEFAULT_QPROFILES" ("QPROFILE_UUID");
+
+
 CREATE TABLE "PROJECT_QPROFILES" (
   "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
   "PROJECT_UUID" VARCHAR(50) NOT NULL,
index d94eb8ef217a08ff72c4b8fdba3f672d782f5b22..d6ad5559e52ab193250898db1628a15f6e45efce 100644 (file)
@@ -54,6 +54,7 @@ import org.sonar.db.qualitygate.ProjectQgateAssociationDao;
 import org.sonar.db.qualitygate.QualityGateConditionDao;
 import org.sonar.db.qualitygate.QualityGateDao;
 import org.sonar.db.qualityprofile.ActiveRuleDao;
+import org.sonar.db.qualityprofile.DefaultQProfileDao;
 import org.sonar.db.qualityprofile.QProfileChangeDao;
 import org.sonar.db.qualityprofile.QualityProfileDao;
 import org.sonar.db.rule.RuleDao;
@@ -83,6 +84,7 @@ public class DaoModule extends Module {
     ComponentKeyUpdaterDao.class,
     ComponentLinkDao.class,
     CustomMeasureDao.class,
+    DefaultQProfileDao.class,
     DuplicationDao.class,
     EventDao.class,
     FileSourceDao.class,
index b88fc81baf52ac901429b6cb3a58e6f78096ece8..f2eb34c6bb1cc01a7df110cacaf81ad9bb1e9b29 100644 (file)
@@ -53,6 +53,7 @@ import org.sonar.db.qualitygate.ProjectQgateAssociationDao;
 import org.sonar.db.qualitygate.QualityGateConditionDao;
 import org.sonar.db.qualitygate.QualityGateDao;
 import org.sonar.db.qualityprofile.ActiveRuleDao;
+import org.sonar.db.qualityprofile.DefaultQProfileDao;
 import org.sonar.db.qualityprofile.QProfileChangeDao;
 import org.sonar.db.qualityprofile.QualityProfileDao;
 import org.sonar.db.rule.RuleDao;
@@ -116,6 +117,7 @@ public class DbClient {
   private final QProfileChangeDao qProfileChangeDao;
   private final UserPermissionDao userPermissionDao;
   private final WebhookDeliveryDao webhookDeliveryDao;
+  private final DefaultQProfileDao defaultQProfileDao;
 
   public DbClient(Database database, MyBatis myBatis, Dao... daos) {
     this.database = database;
@@ -170,6 +172,7 @@ public class DbClient {
     qProfileChangeDao = getDao(map, QProfileChangeDao.class);
     userPermissionDao = getDao(map, UserPermissionDao.class);
     webhookDeliveryDao = getDao(map, WebhookDeliveryDao.class);
+    defaultQProfileDao = getDao(map, DefaultQProfileDao.class);
   }
 
   public DbSession openSession(boolean batch) {
@@ -360,6 +363,10 @@ public class DbClient {
     return webhookDeliveryDao;
   }
 
+  public DefaultQProfileDao defaultQProfileDao() {
+    return defaultQProfileDao;
+  }
+
   protected <K extends Dao> K getDao(Map<Class, Dao> map, Class<K> clazz) {
     return (K) map.get(clazz);
   }
index 0b847ed1d7d91a8bd9151c727ec97d30a48d418c..b65f4ba99683e51a1883508792ac6abc9478919c 100644 (file)
@@ -95,6 +95,7 @@ import org.sonar.db.qualitygate.QualityGateMapper;
 import org.sonar.db.qualityprofile.ActiveRuleDto;
 import org.sonar.db.qualityprofile.ActiveRuleMapper;
 import org.sonar.db.qualityprofile.ActiveRuleParamDto;
+import org.sonar.db.qualityprofile.DefaultQProfileMapper;
 import org.sonar.db.qualityprofile.QProfileChangeMapper;
 import org.sonar.db.qualityprofile.QualityProfileDto;
 import org.sonar.db.qualityprofile.QualityProfileMapper;
@@ -196,6 +197,7 @@ public class MyBatis implements Startable {
       ComponentLinkMapper.class,
       ComponentMapper.class,
       CustomMeasureMapper.class,
+      DefaultQProfileMapper.class,
       DuplicationMapper.class,
       EventMapper.class,
       FileSourceMapper.class,
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/DefaultQProfileDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/DefaultQProfileDao.java
new file mode 100644 (file)
index 0000000..0f62f0b
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.qualityprofile;
+
+import java.util.Collection;
+import java.util.Set;
+import org.sonar.api.utils.System2;
+import org.sonar.db.Dao;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.db.DbSession;
+
+import static java.util.Collections.singletonList;
+
+public class DefaultQProfileDao implements Dao {
+
+  private final System2 system2;
+
+  public DefaultQProfileDao(System2 system2) {
+    this.system2 = system2;
+  }
+
+  public void insertOrUpdate(DbSession dbSession, DefaultQProfileDto dto) {
+    long now = system2.now();
+    DefaultQProfileMapper mapper = mapper(dbSession);
+    if (mapper.update(dto, now) == 0) {
+      mapper.insert(dto, now);
+    }
+  }
+
+  public void deleteByQProfileUuids(DbSession dbSession, Collection<String> qProfileUuids) {
+    DefaultQProfileMapper mapper = mapper(dbSession);
+    DatabaseUtils.executeLargeUpdates(qProfileUuids, mapper::deleteByQProfileUuids);
+  }
+
+  public Set<String> selectExistingQProfileUuids(DbSession dbSession, String organizationUuid, Collection<String> qProfileUuids) {
+    return mapper(dbSession).selectExistingQProfileUuids(organizationUuid, qProfileUuids);
+  }
+
+  public boolean isDefault(DbSession dbSession, String organizationUuid, String qProfileUuid) {
+    return selectExistingQProfileUuids(dbSession, organizationUuid, singletonList(qProfileUuid)).contains(qProfileUuid);
+  }
+
+  private static DefaultQProfileMapper mapper(DbSession dbSession) {
+    return dbSession.getMapper(DefaultQProfileMapper.class);
+  }
+}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/DefaultQProfileDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/DefaultQProfileDto.java
new file mode 100644 (file)
index 0000000..5314540
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.qualityprofile;
+
+public class DefaultQProfileDto {
+
+  private String organizationUuid;
+  private String language;
+  private String qProfileUuid;
+
+  public String getOrganizationUuid() {
+    return organizationUuid;
+  }
+
+  public DefaultQProfileDto setOrganizationUuid(String s) {
+    this.organizationUuid = s;
+    return this;
+  }
+
+  public String getLanguage() {
+    return language;
+  }
+
+  public DefaultQProfileDto setLanguage(String s) {
+    this.language = s;
+    return this;
+  }
+
+  public String getQProfileUuid() {
+    return qProfileUuid;
+  }
+
+  public DefaultQProfileDto setQProfileUuid(String s) {
+    this.qProfileUuid = s;
+    return this;
+  }
+
+  public static DefaultQProfileDto from(QualityProfileDto profile) {
+    return new DefaultQProfileDto()
+      .setOrganizationUuid(profile.getOrganizationUuid())
+      .setLanguage(profile.getLanguage())
+      .setQProfileUuid(profile.getKee());
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder("DefaultQProfileDto{");
+    sb.append("organizationUuid='").append(organizationUuid).append('\'');
+    sb.append(", language='").append(language).append('\'');
+    sb.append(", qProfileUuid='").append(qProfileUuid).append('\'');
+    sb.append('}');
+    return sb.toString();
+  }
+}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/DefaultQProfileMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/DefaultQProfileMapper.java
new file mode 100644 (file)
index 0000000..6f69965
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.qualityprofile;
+
+import java.util.Collection;
+import java.util.Set;
+import org.apache.ibatis.annotations.Param;
+
+public interface DefaultQProfileMapper {
+  void insert(@Param("dto") DefaultQProfileDto dto, @Param("now") long now);
+
+  int update(@Param("dto") DefaultQProfileDto dto, @Param("now") long now);
+
+  void deleteByQProfileUuids(@Param("qProfileUuids") Collection<String> qProfileUuids);
+
+  Set<String> selectExistingQProfileUuids(
+    @Param("organizationUuid") String organizationUuid,
+    @Param("qProfileUuids") Collection<String> qProfileUuids);
+}
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/DefaultQProfileMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/DefaultQProfileMapper.xml
new file mode 100644 (file)
index 0000000..71c6c32
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="org.sonar.db.qualityprofile.DefaultQProfileMapper">
+
+  <insert id="insert" useGeneratedKeys="false" parameterType="map">
+    insert into default_qprofiles
+    (
+      organization_uuid,
+      language,
+      qprofile_uuid,
+      created_at,
+      updated_at
+    ) values (
+      #{dto.organizationUuid, jdbcType=VARCHAR},
+      #{dto.language, jdbcType=VARCHAR},
+      #{dto.qProfileUuid, jdbcType=VARCHAR},
+      #{now, jdbcType=BIGINT},
+      #{now, jdbcType=BIGINT}
+    )
+  </insert>
+
+  <update id="update" parameterType="map">
+    update default_qprofiles
+    set
+      qprofile_uuid = #{dto.qProfileUuid, jdbcType=VARCHAR},
+      updated_at = #{now, jdbcType=BIGINT}
+    where
+      organization_uuid = #{dto.organizationUuid, jdbcType=VARCHAR}
+      and language = #{dto.language, jdbcType=VARCHAR}
+  </update>
+
+  <delete id="deleteByQProfileUuids" parameterType="String">
+    delete from default_qprofiles
+    where qprofile_uuid in
+    <foreach collection="qProfileUuids" open="(" close=")" item="qProfileUuid" separator=",">
+      #{qProfileUuid, jdbcType=VARCHAR}
+    </foreach>
+  </delete>
+
+  <select id="selectExistingQProfileUuids" parameterType="map" resultType="String">
+    select qprofile_uuid
+    from default_qprofiles
+    where
+      organization_uuid = #{organizationUuid, jdbcType=VARCHAR}
+      and qprofile_uuid in
+      <foreach collection="qProfileUuids" open="(" close=")" item="qProfileUuid" separator=",">
+        #{qProfileUuid, jdbcType=VARCHAR}
+      </foreach>
+  </select>
+</mapper>
+
index 7e2cf115298b6a4926aca409ba14e5f756bc72d3..728614064df771bb4bf2bbca3b6d7b7502247945 100644 (file)
     order by created_at desc
   </sql>
 
-  <update id="deleteByProfileKeys" parameterType="String">
+  <delete id="deleteByProfileKeys" parameterType="String">
     delete from qprofile_changes
     where qprofile_key in
     <foreach collection="profileKeys" open="(" close=")" item="profileKey" separator=",">
       #{profileKey, jdbcType=VARCHAR}
     </foreach>
-  </update>
+  </delete>
 </mapper>
 
index 914622cfd1e8feb55b578c9930ad212f1e4388aa..4e2c7cb812e4e4ff162a937bb8da518a5730a8cb 100644 (file)
@@ -29,6 +29,6 @@ public class DaoModuleTest {
   public void verify_count_of_added_components() {
     ComponentContainer container = new ComponentContainer();
     new DaoModule().configure(container);
-    assertThat(container.size()).isEqualTo(2 + 45);
+    assertThat(container.size()).isEqualTo(2 + 46);
   }
 }
index d477fed8cadcdde31572133f944c74a8040815b1..2f922b93fe4be694ca8aca9bb78bf046b2ae18dc 100644 (file)
@@ -31,7 +31,7 @@ public class ActiveRuleParamDtoTest {
 
   @Test
   public void groupByKey() {
-    assertThat(ActiveRuleParamDto.groupByKey(Collections.<ActiveRuleParamDto>emptyList())).isEmpty();
+    assertThat(ActiveRuleParamDto.groupByKey(Collections.emptyList())).isEmpty();
 
     Collection<ActiveRuleParamDto> dtos = Arrays.asList(
       new ActiveRuleParamDto().setKey("foo"), new ActiveRuleParamDto().setKey("bar")
diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/DefaultQProfileDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/DefaultQProfileDaoTest.java
new file mode 100644 (file)
index 0000000..3d48cb4
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.qualityprofile;
+
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.Uuids;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.organization.OrganizationDto;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DefaultQProfileDaoTest {
+
+  @Rule
+  public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+  private DbSession dbSession = dbTester.getSession();
+  private DefaultQProfileDao underTest = dbTester.getDbClient().defaultQProfileDao();
+
+  @Test
+  public void insertOrUpdate_inserts_row_when_does_not_exist() {
+    OrganizationDto org = dbTester.organizations().insert();
+    QualityProfileDto profile = dbTester.qualityProfiles().insert(org);
+    DefaultQProfileDto dto = DefaultQProfileDto.from(profile);
+
+    underTest.insertOrUpdate(dbSession, dto);
+    dbSession.commit();
+
+    assertThat(countRows()).isEqualTo(1);
+    assertThatIsDefault(org, profile);
+  }
+
+  @Test
+  public void insertOrUpdate_updates_row_when_exists() {
+    OrganizationDto org = dbTester.organizations().insert();
+    String previousQProfileUuid = Uuids.create();
+    DefaultQProfileDto dto = new DefaultQProfileDto()
+      .setLanguage("java")
+      .setOrganizationUuid(org.getUuid())
+      .setQProfileUuid(previousQProfileUuid);
+    underTest.insertOrUpdate(dbSession, dto);
+    dbSession.commit();
+
+    String newQProfileUuid = Uuids.create();
+    dto.setQProfileUuid(newQProfileUuid);
+    underTest.insertOrUpdate(dbSession, dto);
+    dbSession.commit();
+
+    assertThat(countRows()).isEqualTo(1);
+    assertThat(dbTester.qualityProfiles().selectUuidOfDefaultProfile(org, dto.getLanguage())).hasValue(newQProfileUuid);
+  }
+
+  @Test
+  public void deleteByQProfileUuids_deletes_rows_related_to_specified_profile() {
+    OrganizationDto org1 = dbTester.organizations().insert();
+    OrganizationDto org2 = dbTester.organizations().insert();
+    underTest.insertOrUpdate(dbSession, new DefaultQProfileDto().setOrganizationUuid(org1.getUuid()).setLanguage("java").setQProfileUuid("u1"));
+    underTest.insertOrUpdate(dbSession, new DefaultQProfileDto().setOrganizationUuid(org1.getUuid()).setLanguage("js").setQProfileUuid("u2"));
+    underTest.insertOrUpdate(dbSession, new DefaultQProfileDto().setOrganizationUuid(org2.getUuid()).setLanguage("java").setQProfileUuid("u3"));
+    underTest.insertOrUpdate(dbSession, new DefaultQProfileDto().setOrganizationUuid(org2.getUuid()).setLanguage("js").setQProfileUuid("u4"));
+
+    underTest.deleteByQProfileUuids(dbSession, asList("u1", "u3"));
+    dbSession.commit();
+
+    assertThat(countRows()).isEqualTo(2);
+    assertThat(dbTester.qualityProfiles().selectUuidOfDefaultProfile(org1, "java")).isEmpty();
+    assertThat(dbTester.qualityProfiles().selectUuidOfDefaultProfile(org1, "js")).hasValue("u2");
+    assertThat(dbTester.qualityProfiles().selectUuidOfDefaultProfile(org2, "java")).isEmpty();
+    assertThat(dbTester.qualityProfiles().selectUuidOfDefaultProfile(org2, "js")).hasValue("u4");
+  }
+
+  @Test
+  public void selectExistingQProfileUuids_filters_defaults() {
+    OrganizationDto org = dbTester.organizations().insert();
+    QualityProfileDto profile1 = dbTester.qualityProfiles().insert(org);
+    QualityProfileDto profile2 = dbTester.qualityProfiles().insert(org);
+    dbTester.qualityProfiles().markAsDefault(profile1);
+
+    List<String> profileUuids = asList(profile1.getKee(), profile2.getKee(), "other");
+    assertThat(underTest.selectExistingQProfileUuids(dbSession, org.getUuid(), profileUuids))
+      .containsExactly(profile1.getKee());
+  }
+
+  @Test
+  public void isDefault_returns_true_if_profile_is_marked_as_default() {
+    OrganizationDto org = dbTester.organizations().insert();
+    QualityProfileDto profile1 = dbTester.qualityProfiles().insert(org);
+    QualityProfileDto profile2 = dbTester.qualityProfiles().insert(org);
+    dbTester.qualityProfiles().markAsDefault(profile1);
+
+    assertThat(underTest.isDefault(dbSession, org.getUuid(), profile1.getKee())).isTrue();
+    assertThat(underTest.isDefault(dbSession, org.getUuid(), profile2.getKee())).isFalse();
+    assertThat(underTest.isDefault(dbSession, org.getUuid(), "does_not_exist")).isFalse();
+  }
+
+  private void assertThatIsDefault(OrganizationDto org, QualityProfileDto profile) {
+    assertThat(dbTester.qualityProfiles().selectUuidOfDefaultProfile(org, profile.getLanguage())).hasValue(profile.getKee());
+    assertThat(underTest.isDefault(dbSession, org.getUuid(), profile.getKee())).isTrue();
+  }
+
+  private int countRows() {
+    return dbTester.countRowsOfTable("default_qprofiles");
+  }
+}
index 555acd936c878bfe7a5a4952b83ff96c1804873f..42240eba66a592cbb9d719e665041d0fcf06980d 100644 (file)
@@ -34,12 +34,14 @@ import static org.sonar.api.rule.Severity.MAJOR;
 import static org.sonar.db.qualityprofile.ActiveRuleDto.createFor;
 
 public class QualityProfileDbTester {
+  private final DbTester dbTester;
   private final DbClient dbClient;
   private final DbSession dbSession;
 
-  public QualityProfileDbTester(DbTester db) {
-    this.dbClient = db.getDbClient();
-    this.dbSession = db.getSession();
+  public QualityProfileDbTester(DbTester dbTester) {
+    this.dbTester = dbTester;
+    this.dbClient = dbTester.getDbClient();
+    this.dbSession = dbTester.getSession();
   }
 
   public Optional<QualityProfileDto> selectByKey(String key) {
@@ -98,4 +100,22 @@ public class QualityProfileDbTester {
     dbSession.commit();
     return activeRule;
   }
+
+  public void markAsDefault(QualityProfileDto profile) {
+    DefaultQProfileDto dto = new DefaultQProfileDto()
+      .setOrganizationUuid(profile.getOrganizationUuid())
+      .setLanguage(profile.getLanguage())
+      .setQProfileUuid(profile.getKee());
+    dbClient.defaultQProfileDao().insertOrUpdate(dbSession, dto);
+    dbSession.commit();
+  }
+
+  public Optional<String> selectUuidOfDefaultProfile(OrganizationDto org, String language) {
+    return dbTester.select("select qprofile_uuid as \"profileUuid\" " +
+      " from default_qprofiles " +
+      " where organization_uuid='" + org.getUuid() + "' and language='" + language + "'")
+      .stream()
+      .findFirst()
+      .map(m -> (String)m.get("profileUuid"));
+  }
 }
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v65/CreateTableDefaultQProfiles.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v65/CreateTableDefaultQProfiles.java
new file mode 100644 (file)
index 0000000..aae41a0
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v65;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.sql.CreateTableBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+public class CreateTableDefaultQProfiles extends DdlChange {
+  public CreateTableDefaultQProfiles(Database db) {
+    super(db);
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    VarcharColumnDef profileUuidColumn = newVarcharColumnDefBuilder()
+      .setColumnName("qprofile_uuid")
+      .setLimit(UUID_SIZE)
+      .setIsNullable(false)
+      .setIgnoreOracleUnit(true)
+      .build();
+    context.execute(
+      new CreateTableBuilder(getDialect(), "default_qprofiles")
+        .addPkColumn(newVarcharColumnDefBuilder()
+          .setColumnName("organization_uuid")
+          .setLimit(UUID_SIZE)
+          .setIsNullable(false)
+          .setIgnoreOracleUnit(true)
+          .build())
+        .addPkColumn(newVarcharColumnDefBuilder()
+          .setColumnName("language")
+          .setLimit(20)
+          .setIsNullable(false)
+          .setIgnoreOracleUnit(true)
+          .build())
+        .addColumn(profileUuidColumn)
+        .addColumn(newBigIntegerColumnDefBuilder()
+          .setColumnName("created_at")
+          .setIsNullable(false)
+          .build())
+        .addColumn(newBigIntegerColumnDefBuilder()
+          .setColumnName("updated_at")
+          .setIsNullable(false)
+          .build())
+        .build());
+
+    context.execute(
+      new CreateIndexBuilder(getDialect())
+        .setTable("default_qprofiles")
+        .setName("uniq_default_qprofiles_uuid")
+        .addColumn(profileUuidColumn)
+        .setUnique(true)
+        .build());
+  }
+}
index 52bd522c806ce146f2a674d1d0892360c0c5521b..4bad1e1ce306e50a47daa658758163c9f9ad3f1c 100644 (file)
@@ -44,6 +44,8 @@ public class DbVersion65 implements DbVersion {
       .add(1715, "Add rules_profiles.is_built_in", AddBuiltInFlagToRulesProfiles.class)
       .add(1716, "Set rules_profiles.is_built_in to false", SetRulesProfilesIsBuiltInToFalse.class)
       .add(1717, "Make rules_profiles.is_built_in not null", MakeRulesProfilesIsBuiltInNotNullable.class)
-      .add(1718, "Delete unused loaded_templates on quality profiles", DeleteLoadedTemplatesOnQProfiles.class);
+      .add(1718, "Delete unused loaded_templates on quality profiles", DeleteLoadedTemplatesOnQProfiles.class)
+      .add(1719, "Create table default_qprofiles", CreateTableDefaultQProfiles.class)
+      .add(1720, "Populate table default_qprofiles", PopulateTableDefaultQProfiles.class);
   }
 }
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v65/PopulateTableDefaultQProfiles.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v65/PopulateTableDefaultQProfiles.java
new file mode 100644 (file)
index 0000000..df0a864
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v65;
+
+import java.sql.SQLException;
+import java.util.HashSet;
+import java.util.Set;
+import org.sonar.api.utils.System2;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+import org.sonar.server.platform.db.migration.step.Select;
+
+/**
+ * Move the list of profiles marked as "default" from rules_profiles.is_default
+ * to the table default_qprofiles.
+ */
+public class PopulateTableDefaultQProfiles extends DataChange {
+
+  private final System2 system2;
+
+  public PopulateTableDefaultQProfiles(Database db, System2 system2) {
+    super(db);
+    this.system2 = system2;
+  }
+
+  @Override
+  protected void execute(Context context) throws SQLException {
+    Set<OrgLang> buggyOrgs = selectOrgsWithMultipleDefaultProfiles(context);
+    Set<OrgLang> processedBuggyOrgs = new HashSet<>();
+    long now = system2.now();
+
+    MassUpdate massUpdate = context.prepareMassUpdate();
+    massUpdate.select("select p.organization_uuid, p.language, p.kee from rules_profiles p " +
+      " where p.is_default = ? " +
+      " and not exists (select 1 from default_qprofiles dp where dp.organization_uuid = p.organization_uuid and dp.language = p.language)" +
+      " order by id")
+      .setBoolean(1, true);
+    massUpdate.update("insert into default_qprofiles" +
+      " (organization_uuid, language, qprofile_uuid, created_at, updated_at) values (?, ?, ?, ?, ?)");
+    massUpdate.rowPluralName("default_qprofiles");
+    massUpdate.execute((row, update) -> {
+      OrgLang pk = new OrgLang(row.getString(1), row.getString(2));
+      String profileUuid = row.getString(3);
+
+      boolean isBuggy = buggyOrgs.contains(pk);
+      if (isBuggy && processedBuggyOrgs.contains(pk)) {
+        // profile is ignored. There's already one marked as default.
+        return false;
+      }
+      update.setString(1, pk.orgUuid);
+      update.setString(2, pk.language);
+      update.setString(3, profileUuid);
+      update.setLong(4, now);
+      update.setLong(5, now);
+      if (isBuggy) {
+        processedBuggyOrgs.add(pk);
+      }
+      return true;
+    });
+  }
+
+  /**
+   * By design the table rules_profiles does not enforce to have a single
+   * profile marked as default for an organization and language.
+   * This method returns the buggy rows.
+   */
+  private Set<OrgLang> selectOrgsWithMultipleDefaultProfiles(Context context) throws SQLException {
+    Select rows = context.prepareSelect("select organization_uuid, language from rules_profiles " +
+      " where is_default = ? " +
+      " group by organization_uuid, language " +
+      " having count(kee) > 1 ")
+      .setBoolean(1, true);
+    return new HashSet<>(rows.list(row -> new OrgLang(row.getString(1), row.getString(2))));
+  }
+
+  private static class OrgLang {
+    private final String orgUuid;
+    private final String language;
+
+    private OrgLang(String orgUuid, String language) {
+      this.orgUuid = orgUuid;
+      this.language = language;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+
+      OrgLang orgLang = (OrgLang) o;
+      if (!orgUuid.equals(orgLang.orgUuid)) {
+        return false;
+      }
+      return language.equals(orgLang.language);
+    }
+
+    @Override
+    public int hashCode() {
+      int result = orgUuid.hashCode();
+      result = 31 * result + language.hashCode();
+      return result;
+    }
+  }
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v65/CreateTableDefaultQProfilesTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v65/CreateTableDefaultQProfilesTest.java
new file mode 100644 (file)
index 0000000..88fb353
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.platform.db.migration.version.v65;
+
+import java.sql.SQLException;
+import java.sql.Types;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.db.CoreDbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CreateTableDefaultQProfilesTest {
+  private static final String TABLE = "default_qprofiles";
+
+  @Rule
+  public final CoreDbTester db = CoreDbTester.createForSchema(CreateTableDefaultQProfilesTest.class, "empty.sql");
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private CreateTableDefaultQProfiles underTest = new CreateTableDefaultQProfiles(db.database());
+
+  @Test
+  public void creates_table_on_empty_db() throws SQLException {
+    underTest.execute();
+
+    assertThat(db.countRowsOfTable(TABLE)).isEqualTo(0);
+
+    db.assertColumnDefinition(TABLE, "organization_uuid", Types.VARCHAR, 40, false);
+    db.assertColumnDefinition(TABLE, "language", Types.VARCHAR, 20, false);
+    db.assertColumnDefinition(TABLE, "qprofile_uuid", Types.VARCHAR, 40, false);
+    db.assertColumnDefinition(TABLE, "created_at", Types.BIGINT, null, false);
+    db.assertColumnDefinition(TABLE, "updated_at", Types.BIGINT, null, false);
+    db.assertPrimaryKey(TABLE, "pk_" + TABLE, "organization_uuid", "language");
+    db.assertUniqueIndex(TABLE, "uniq_default_qprofiles_uuid", "qprofile_uuid");
+  }
+
+  @Test
+  public void migration_is_not_reentrant() throws SQLException {
+    underTest.execute();
+
+    expectedException.expect(IllegalStateException.class);
+
+    underTest.execute();
+  }
+
+}
index 8cd51244d00bd14266e76750d2093477aabb5182..aac62ae653007758296c5f4a3a3646dc5472a075 100644 (file)
@@ -35,6 +35,6 @@ public class DbVersion65Test {
 
   @Test
   public void verify_migration_count() {
-    verifyMigrationCount(underTest, 19);
+    verifyMigrationCount(underTest, 21);
   }
 }
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v65/PopulateTableDefaultQProfilesTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v65/PopulateTableDefaultQProfilesTest.java
new file mode 100644 (file)
index 0000000..47af84a
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v65;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.internal.AlwaysIncreasingSystem2;
+import org.sonar.db.CoreDbTester;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+public class PopulateTableDefaultQProfilesTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Rule
+  public CoreDbTester db = CoreDbTester.createForSchema(PopulateTableDefaultQProfilesTest.class, "initial.sql");
+
+  private System2 system2 = new AlwaysIncreasingSystem2();
+  private PopulateTableDefaultQProfiles underTest = new PopulateTableDefaultQProfiles(db.database(), system2);
+
+  @Test
+  public void migration_is_reentrant() throws SQLException {
+    insertRulesProfile("ORG_1", "java", "u1", true);
+    insertRulesProfile("ORG_2", "js", "u2", true);
+
+    // org1 is already processed
+    insertDefaultQProfile("ORG_1", "java", "u1");
+
+    underTest.execute();
+
+    assertThat(countRows()).isEqualTo(2);
+    assertThat(selectDefaultProfile("ORG_1", "java")).isEqualTo("u1");
+    assertThat(selectDefaultProfile("ORG_2", "js")).isEqualTo("u2");
+  }
+
+  @Test
+  public void DEFAULT_QPROFILES_is_populated_by_copying_the_RULES_PROFILES_marked_as_default() throws SQLException {
+    insertRulesProfile("ORG_1", "java", "u1", false);
+    insertRulesProfile("ORG_1", "java", "u2", true);
+    insertRulesProfile("ORG_1", "js", "u3", true);
+
+    underTest.execute();
+
+    assertThat(countRows()).isEqualTo(2);
+    assertThat(selectDefaultProfile("ORG_1", "java")).isEqualTo("u2");
+    assertThat(selectDefaultProfile("ORG_1", "js")).isEqualTo("u3");
+  }
+
+  @Test
+  public void duplicated_rows_of_table_RULES_PROFILES_are_ignored() throws SQLException {
+    // two java profiles are marked as default.
+    // The second one (as ordered by id) is ignored.
+    insertRulesProfile("ORG_1", "java", "u1", false);
+    insertRulesProfile("ORG_1", "java", "u2", true);
+    insertRulesProfile("ORG_1", "java", "u3", true);
+
+    underTest.execute();
+
+    assertThat(countRows()).isEqualTo(1);
+    assertThat(selectDefaultProfile("ORG_1", "java")).isEqualTo("u2");
+  }
+
+  private int countRows() {
+    return db.countRowsOfTable("default_qprofiles");
+  }
+
+  private void insertRulesProfile(String orgUuid, String language, String uuid, boolean isDefault) {
+    db.executeInsert("RULES_PROFILES",
+      "NAME", "name_" + uuid,
+      "KEE", uuid,
+      "ORGANIZATION_UUID", orgUuid,
+      "LANGUAGE", language,
+      "IS_DEFAULT", isDefault,
+      "IS_BUILT_IN", true);
+  }
+
+  private void insertDefaultQProfile(String orgUuid, String language, String uuid) {
+    db.executeInsert("DEFAULT_QPROFILES",
+      "ORGANIZATION_UUID", orgUuid,
+      "LANGUAGE", language,
+      "QPROFILE_UUID", uuid);
+  }
+
+  private String selectDefaultProfile(String orgUuid, String language) {
+    return (String)db.selectFirst("select qprofile_uuid as QPROFILE_UUID from default_qprofiles where organization_uuid='" + orgUuid + "' and language='" + language + "'")
+      .get("QPROFILE_UUID");
+  }
+
+}
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v65/CreateTableDefaultQProfilesTest/empty.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v65/CreateTableDefaultQProfilesTest/empty.sql
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v65/PopulateTableDefaultQProfilesTest/initial.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v65/PopulateTableDefaultQProfilesTest/initial.sql
new file mode 100644 (file)
index 0000000..b114523
--- /dev/null
@@ -0,0 +1,26 @@
+CREATE TABLE "RULES_PROFILES" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+  "NAME" VARCHAR(100) NOT NULL,
+  "LANGUAGE" VARCHAR(20),
+  "ORGANIZATION_UUID" VARCHAR(40) NOT NULL,
+  "KEE" VARCHAR(255) NOT NULL,
+  "PARENT_KEE" VARCHAR(255),
+  "RULES_UPDATED_AT" VARCHAR(100),
+  "IS_DEFAULT" BOOLEAN NOT NULL DEFAULT FALSE,
+  "CREATED_AT" TIMESTAMP,
+  "UPDATED_AT" TIMESTAMP,
+  "LAST_USED" BIGINT,
+  "USER_UPDATED_AT" BIGINT,
+  "IS_BUILT_IN" BOOLEAN NOT NULL
+);
+CREATE UNIQUE INDEX "UNIQ_QPROF_KEY" ON "RULES_PROFILES" ("KEE");
+
+
+CREATE TABLE "DEFAULT_QPROFILES" (
+  "ORGANIZATION_UUID" VARCHAR(40) NOT NULL,
+  "LANGUAGE" VARCHAR(20) NOT NULL,
+  "QPROFILE_UUID" VARCHAR(40) NOT NULL,
+  "CREATED_AT" BIGINT,
+  "UPDATED_AT" BIGINT
+);
+CREATE PRIMARY KEY ON "DEFAULT_QPROFILES" ("ORGANIZATION_UUID", "LANGUAGE");
index bbe30e9593e1efd3974515b1404ae3dbbc4dbe4f..fe42eeb3489e23f49bd2535f666cb2e594c66243 100644 (file)
@@ -46,6 +46,7 @@ import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.qualityprofile.ActiveRuleDto;
 import org.sonar.db.qualityprofile.ActiveRuleKey;
 import org.sonar.db.qualityprofile.ActiveRuleParamDto;
+import org.sonar.db.qualityprofile.DefaultQProfileDto;
 import org.sonar.db.qualityprofile.QualityProfileDto;
 import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.db.rule.RuleParamDto;
@@ -90,7 +91,7 @@ public class BuiltInQProfileInsertImpl implements BuiltInQProfileInsert {
     }
   }
 
-  private QualityProfileDto insertQualityProfile(DbSession session, BuiltInQProfile builtInQProfile, OrganizationDto organization, Date now) {
+  private QualityProfileDto insertQualityProfile(DbSession dbSession, BuiltInQProfile builtInQProfile, OrganizationDto organization, Date now) {
     QualityProfileDto profileDto = QualityProfileDto.createFor(uuidFactory.create())
       .setName(builtInQProfile.getName())
       .setOrganizationUuid(organization.getUuid())
@@ -98,7 +99,10 @@ public class BuiltInQProfileInsertImpl implements BuiltInQProfileInsert {
       .setDefault(builtInQProfile.isDefault())
       .setIsBuiltIn(true)
       .setRulesUpdatedAtAsDate(now);
-    dbClient.qualityProfileDao().insert(session, profileDto);
+    dbClient.qualityProfileDao().insert(dbSession, profileDto);
+    if (builtInQProfile.isDefault()) {
+      dbClient.defaultQProfileDao().insertOrUpdate(dbSession, DefaultQProfileDto.from(profileDto));
+    }
     return profileDto;
   }
 
index 35978d7e3c48cf9387a8a5a3c843c8d0d052d619..4ac28e9e5860a66d7432480bf57803e984820115 100644 (file)
@@ -29,6 +29,7 @@ import org.sonar.core.util.UuidFactory;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.qualityprofile.DefaultQProfileDto;
 import org.sonar.db.qualityprofile.QualityProfileDto;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
@@ -102,6 +103,9 @@ public class QProfileFactory {
       .setIsBuiltIn(isBuiltIn)
       .setRulesUpdatedAtAsDate(now);
     db.qualityProfileDao().insert(dbSession, dto);
+    if (isDefault) {
+      db.defaultQProfileDao().insertOrUpdate(dbSession, DefaultQProfileDto.from(dto));
+    }
     return dto;
   }
 
@@ -119,6 +123,7 @@ public class QProfileFactory {
       db.activeRuleDao().deleteParametersByProfileKeys(dbSession, profileKeys);
       db.activeRuleDao().deleteByProfileKeys(dbSession, profileKeys);
       db.qProfileChangeDao().deleteByProfileKeys(dbSession, profileKeys);
+      db.defaultQProfileDao().deleteByQProfileUuids(dbSession, profileKeys);
       db.qualityProfileDao().deleteByKeys(dbSession, profileKeys);
       dbSession.commit();
       activeRuleIndexer.deleteByProfileKeys(profileKeys);
index 8d2c3ea501ff0e1139a76f633b20b3721b7d46b0..4abe22cd6390cc412670047d0763337566e21481 100644 (file)
@@ -83,6 +83,7 @@ public class CopyAction implements QProfileWsAction {
       userSession.checkPermission(OrganizationPermission.ADMINISTER_QUALITY_PROFILES, sourceProfile.getOrganizationUuid());
 
       QualityProfileDto copiedProfile = profileCopier.copyToName(dbSession, sourceProfile, newName);
+      boolean isDefault = dbClient.defaultQProfileDao().isDefault(dbSession, copiedProfile.getOrganizationUuid(), copiedProfile.getKee());
 
       String languageKey = copiedProfile.getLanguage();
       Language language = languages.get(copiedProfile.getLanguage());
@@ -93,7 +94,7 @@ public class CopyAction implements QProfileWsAction {
           .prop("name", copiedProfile.getName())
           .prop("language", languageKey)
           .prop("languageName", language == null ? null : language.getName())
-          .prop("isDefault", copiedProfile.isDefault())
+          .prop("isDefault", isDefault)
           .prop("isInherited", parentKey != null)
           .prop("parentKey", parentKey)
           .endObject().close();
index b8c110f75197fc3942564dbf995df673202bd787..098321089ba7ec11a14c8d782b413b9ca1bf27cd 100644 (file)
@@ -19,7 +19,9 @@
  */
 package org.sonar.server.qualityprofile.ws;
 
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.stream.Stream;
 import org.sonar.api.resources.Languages;
 import org.sonar.api.server.ws.Request;
@@ -74,7 +76,7 @@ public class DeleteAction implements QProfileWsAction {
       userSession.checkPermission(ADMINISTER_QUALITY_PROFILES, profile.getOrganizationUuid());
 
       List<QualityProfileDto> descendants = selectDescendants(dbSession, profile);
-      ensureNoneIsMarkedAsDefault(profile, descendants);
+      ensureNoneIsMarkedAsDefault(dbSession, profile, descendants);
 
       profileFactory.deleteByKeys(dbSession, toKeys(profile, descendants));
       dbSession.commit();
@@ -86,10 +88,16 @@ public class DeleteAction implements QProfileWsAction {
     return dbClient.qualityProfileDao().selectDescendants(dbSession, profile.getKey());
   }
 
-  private static void ensureNoneIsMarkedAsDefault(QualityProfileDto profile, List<QualityProfileDto> descendants) {
-    checkArgument(!profile.isDefault(), "Profile '%s' cannot be deleted because it is marked as default", profile.getName());
+  private void ensureNoneIsMarkedAsDefault(DbSession dbSession, QualityProfileDto profile, List<QualityProfileDto> descendants) {
+    Set<String> allUuids = new HashSet<>();
+    allUuids.add(profile.getKee());
+    descendants.forEach(p -> allUuids.add(p.getKee()));
+
+    Set<String> uuidsOfDefaultProfiles = dbClient.defaultQProfileDao().selectExistingQProfileUuids(dbSession, profile.getOrganizationUuid(), allUuids);
+
+    checkArgument(!uuidsOfDefaultProfiles.contains(profile.getKee()), "Profile '%s' cannot be deleted because it is marked as default", profile.getName());
     descendants.stream()
-      .filter(QualityProfileDto::isDefault)
+      .filter(p -> uuidsOfDefaultProfiles.contains(p.getKee()))
       .findFirst()
       .ifPresent(p -> {
         throw new IllegalArgumentException(String.format("Profile '%s' cannot be deleted because its descendant named '%s' is marked as default", profile.getName(), p.getName()));
index d97f243975a5717dda2f4e8c3648e2e669d9a9a9..486d2c9af0ce2c1d17aaa824396075ff7f9bee3f 100644 (file)
@@ -27,6 +27,7 @@ import org.sonar.api.server.ws.WebService.NewAction;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.qualityprofile.DefaultQProfileDto;
 import org.sonar.db.qualityprofile.QualityProfileDto;
 import org.sonar.server.user.UserSession;
 
@@ -77,11 +78,12 @@ public class SetDefaultAction implements QProfileWsAction {
     response.noContent();
   }
 
-  public void setDefault(DbSession session, OrganizationDto organization, QualityProfileDto qualityProfile) {
-    QualityProfileDto previousDefault = dbClient.qualityProfileDao().selectDefaultProfile(session, organization, qualityProfile.getLanguage());
+  public void setDefault(DbSession dbSession, OrganizationDto organization, QualityProfileDto profile) {
+    QualityProfileDto previousDefault = dbClient.qualityProfileDao().selectDefaultProfile(dbSession, organization, profile.getLanguage());
     if (previousDefault != null) {
-      dbClient.qualityProfileDao().update(session, previousDefault.setDefault(false));
+      dbClient.qualityProfileDao().update(dbSession, previousDefault.setDefault(false));
     }
-    dbClient.qualityProfileDao().update(session, qualityProfile.setDefault(true));
+    dbClient.qualityProfileDao().update(dbSession, profile.setDefault(true));
+    dbClient.defaultQProfileDao().insertOrUpdate(dbSession, DefaultQProfileDto.from(profile));
   }
 }
index a5b46dd32ece9a4b4c5cacdfa67bd2e8e36ae1ad..761356581da72201a1df0692669b1344b54434dc 100644 (file)
@@ -144,7 +144,7 @@ public class CopyActionTest {
       "  \"name\": \"" + targetProfile.getName() + "\"," +
       "  \"language\": \"lang1\"," +
       "  \"languageName\": \"Lang1\"," +
-      "  \"isDefault\": " + targetProfile.isDefault() + "," +
+      "  \"isDefault\": false," +
       "  \"isInherited\": false" +
       "}");
     QualityProfileDto loadedProfile = db.getDbClient().qualityProfileDao().selectByKey(db.getSession(), targetProfile.getKey());
index ce703582c6284eb7d74f0300272d53ce3042d4f6..0d330cd3067a2210933103ff9ceac6ce613be333 100644 (file)
@@ -240,7 +240,8 @@ public class DeleteActionTest {
   @Test
   public void throw_ISE_if_deleting_default_profile() {
     OrganizationDto organization = dbTester.organizations().insert();
-    QualityProfileDto profile = createDefaultProfile(organization);
+    QualityProfileDto profile = createProfile(organization);
+    dbTester.qualityProfiles().markAsDefault(profile);
     logInAsQProfileAdministrator(organization);
 
     expectedException.expect(IllegalArgumentException.class);
@@ -256,8 +257,9 @@ public class DeleteActionTest {
   public void throw_ISE_if_a_descendant_is_marked_as_default() {
     OrganizationDto organization = dbTester.organizations().insert();
     QualityProfileDto parentProfile = createProfile(organization);
-    QualityProfileDto childProfile = dbTester.qualityProfiles().insert(organization, p -> p.setLanguage(A_LANGUAGE), p -> p.setDefault(true),
+    QualityProfileDto childProfile = dbTester.qualityProfiles().insert(organization, p -> p.setLanguage(A_LANGUAGE),
       p -> p.setParentKee(parentProfile.getKey()));
+    dbTester.qualityProfiles().markAsDefault(childProfile);
     logInAsQProfileAdministrator(organization);
 
     expectedException.expect(IllegalArgumentException.class);
@@ -286,10 +288,6 @@ public class DeleteActionTest {
   }
 
   private QualityProfileDto createProfile(OrganizationDto organization) {
-    return dbTester.qualityProfiles().insert(organization, p -> p.setLanguage(A_LANGUAGE), p -> p.setDefault(false));
-  }
-
-  private QualityProfileDto createDefaultProfile(OrganizationDto organization) {
-    return dbTester.qualityProfiles().insert(organization, p -> p.setLanguage(A_LANGUAGE), p -> p.setDefault(true));
+    return dbTester.qualityProfiles().insert(organization, p -> p.setLanguage(A_LANGUAGE));
   }
 }