]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8089 Create new MeasureDao#selectTreeByQuery
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Fri, 4 Nov 2016 14:05:13 +0000 (15:05 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Tue, 8 Nov 2016 10:12:51 +0000 (11:12 +0100)
13 files changed:
sonar-db/src/main/java/org/sonar/db/component/ComponentDao.java
sonar-db/src/main/java/org/sonar/db/component/ComponentMapper.java
sonar-db/src/main/java/org/sonar/db/component/ComponentTreeQuery.java
sonar-db/src/main/java/org/sonar/db/measure/MeasureDao.java
sonar-db/src/main/java/org/sonar/db/measure/MeasureMapper.java
sonar-db/src/main/java/org/sonar/db/measure/MeasureQuery.java
sonar-db/src/main/java/org/sonar/db/measure/MeasureTreeQuery.java [new file with mode: 0644]
sonar-db/src/main/resources/org/sonar/db/component/ComponentMapper.xml
sonar-db/src/main/resources/org/sonar/db/measure/MeasureMapper.xml
sonar-db/src/test/java/org/sonar/db/component/ComponentDaoTest.java
sonar-db/src/test/java/org/sonar/db/component/ComponentTreeQueryTest.java
sonar-db/src/test/java/org/sonar/db/measure/MeasureDaoTest.java
sonar-db/src/test/java/org/sonar/db/measure/MeasureTreeQueryTest.java [new file with mode: 0644]

index 62c4652b05df6739ee17dbf6e5bd84a89c14998a..16e4dab6eac3b45cc7b13a9f27204a55dcd68a40 100644 (file)
@@ -175,7 +175,7 @@ public class ComponentDao implements Dao {
       return emptyList();
     }
     ComponentDto component = componentOpt.get();
-    return mapper(dbSession).selectDescendants(query, query.getUuidPath(component));
+    return mapper(dbSession).selectDescendants(query, componentOpt.get().uuid(), query.getUuidPath(component));
   }
 
   public ComponentDto selectOrFailByKey(DbSession session, String key) {
index 7e41d5c97b480ca78717ea2a6b0239313a11a496..6badff92945a90e6a53b4aacf2263a5e5b130516 100644 (file)
@@ -64,7 +64,7 @@ public interface ComponentMapper {
 
   List<ComponentDto> selectAncestors(@Param("query") ComponentTreeQuery query, @Param("baseUuidPathLike") String baseUuidPathLike);
 
-  List<ComponentDto> selectDescendants(@Param("query") ComponentTreeQuery query, @Param("baseUuidPath") String baseUuidPath);
+  List<ComponentDto> selectDescendants(@Param("query") ComponentTreeQuery query, @Param("baseUuid") String baseUuid, @Param("baseUuidPath") String baseUuidPath);
 
   /**
    * Return all project (PRJ/TRK) uuids
index c8f47f6cd75d73da472fbd3af613928115e0d270..dff3bc3144808b3d0574b4a67609b8dd615dfb1e 100644 (file)
@@ -68,6 +68,7 @@ public class ComponentTreeQuery {
     this.sqlSort = builder.sortFields != null ? sortFieldsToSqlSort(builder.sortFields, direction) : null;
   }
 
+  @CheckForNull
   public Collection<String> getQualifiers() {
     return qualifiers;
   }
index 9deba33c76c2df87efa3ec47e4be93ab5a8fc5b4..0daf18d49b1819f58707229c653ab02ad0b6d8e6 100644 (file)
@@ -28,6 +28,7 @@ import java.util.Optional;
 import org.apache.ibatis.session.ResultHandler;
 import org.sonar.db.Dao;
 import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
 
 import static org.sonar.db.DatabaseUtils.executeLargeInputs;
 import static org.sonar.db.DatabaseUtils.executeLargeInputsWithoutOutput;
@@ -103,6 +104,13 @@ public class MeasureDao implements Dao {
     mapper(dbSession).selectByQueryOnSingleComponent(query, resultHandler);
   }
 
+  public List<MeasureDto> selectTreeByQuery(DbSession dbSession, ComponentDto baseComponent, MeasureTreeQuery query) {
+    if (query.returnsEmpty()) {
+      return Collections.emptyList();
+    }
+    return mapper(dbSession).selectTreeByQuery(query, baseComponent.uuid(), query.getUuidPath(baseComponent));
+  }
+
   public List<PastMeasureDto> selectPastMeasures(DbSession dbSession,
     String componentUuid,
     String analysisUuid,
index 7b385815f207bbd84a6a48e74ea55c062d2452c6..978b8b0c47c3b6b67bcce8be7d263439210a2b2f 100644 (file)
@@ -38,6 +38,8 @@ public interface MeasureMapper {
 
   void selectByQueryOnSingleComponent(@Param("query") MeasureQuery query, ResultHandler resultHandler);
 
+  List<MeasureDto> selectTreeByQuery(@Param("query") MeasureTreeQuery measureQuery, @Param("baseUuid") String baseUuid, @Param("baseUuidPath") String baseUuidPath);
+
   List<PastMeasureDto> selectPastMeasures(@Param("componentUuid") String componentUuid, @Param("analysisUuid") String analysisUuid, @Param("metricIds") List<Integer> metricIds);
 
   List<MeasureDto> selectProjectMeasuresOfDeveloper(@Param("developerId") long developerId, @Param("metricIds") Collection<Integer> metricIds);
index 382c44f5313c6228e421deb248d62022bbebd6ae..ec557a7b7462428dc03bf8e7810d40ca69f621a2 100644 (file)
@@ -29,6 +29,7 @@ import static java.util.Collections.singleton;
 import static java.util.Objects.requireNonNull;
 
 public class MeasureQuery {
+  @CheckForNull
   private final String analysisUuid;
 
   @CheckForNull
@@ -136,7 +137,7 @@ public class MeasureQuery {
       return false;
     }
     MeasureQuery that = (MeasureQuery) o;
-    return analysisUuid.equals(that.analysisUuid) &&
+    return Objects.equals(analysisUuid, that.analysisUuid) &&
       Objects.equals(projectUuids, that.projectUuids) &&
       Objects.equals(componentUuids, that.componentUuids) &&
       Objects.equals(metricIds, that.metricIds) &&
diff --git a/sonar-db/src/main/java/org/sonar/db/measure/MeasureTreeQuery.java b/sonar-db/src/main/java/org/sonar/db/measure/MeasureTreeQuery.java
new file mode 100644 (file)
index 0000000..8ccd5a6
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.measure;
+
+import java.util.Collection;
+import java.util.Locale;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.db.WildcardPosition;
+import org.sonar.db.component.ComponentDto;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.db.DatabaseUtils.buildLikeValue;
+import static org.sonar.db.WildcardPosition.AFTER;
+
+public class MeasureTreeQuery {
+
+  public enum Strategy {
+    CHILDREN, LEAVES
+  }
+
+  @CheckForNull
+  private final String nameOrKeyQuery;
+  // SONAR-7681 a public implementation of List must be used in MyBatis - potential concurrency exceptions otherwise
+  @CheckForNull
+  private final Collection<String> qualifiers;
+  private final Strategy strategy;
+
+  @CheckForNull
+  private final Collection<Integer> metricIds;
+
+  @CheckForNull
+  private final Long personId;
+
+  private MeasureTreeQuery(Builder builder) {
+    this.nameOrKeyQuery = builder.nameOrKeyQuery;
+    this.qualifiers = builder.qualifiers == null ? null : newArrayList(builder.qualifiers);
+    this.strategy = requireNonNull(builder.strategy);
+    this.metricIds = builder.metricIds;
+    this.personId = builder.personId;
+  }
+
+  @CheckForNull
+  public String getNameOrKeyQuery() {
+    return nameOrKeyQuery;
+  }
+
+  @CheckForNull
+  public String getNameOrKeyQueryToSqlForResourceIndex() {
+    return nameOrKeyQuery == null ? null : buildLikeValue(nameOrKeyQuery, AFTER).toLowerCase(Locale.ENGLISH);
+  }
+
+  @CheckForNull
+  public Collection<String> getQualifiers() {
+    return qualifiers;
+  }
+
+  public Strategy getStrategy() {
+    return strategy;
+  }
+
+  @CheckForNull
+  public Collection<Integer> getMetricIds() {
+    return metricIds;
+  }
+
+  @CheckForNull
+  public Long getPersonId() {
+    return personId;
+  }
+
+  public String getUuidPath(ComponentDto component) {
+    switch (strategy) {
+      case CHILDREN:
+        return component.getUuidPath() + component.uuid() + ".";
+      case LEAVES:
+        return buildLikeValue(component.getUuidPath() + component.uuid() + ".", WildcardPosition.AFTER);
+      default:
+        throw new IllegalArgumentException("Unknown strategy : " + strategy);
+    }
+  }
+
+  public boolean returnsEmpty() {
+    return (metricIds != null && metricIds.isEmpty()) || (qualifiers != null && qualifiers.isEmpty());
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static final class Builder {
+
+    @CheckForNull
+    private String nameOrKeyQuery;
+    @CheckForNull
+    private Collection<String> qualifiers;
+    private Strategy strategy;
+
+    @CheckForNull
+    private Collection<Integer> metricIds;
+
+    @CheckForNull
+    private Long personId;
+
+    private Builder() {
+    }
+
+    public Builder setNameOrKeyQuery(@Nullable String nameOrKeyQuery) {
+      this.nameOrKeyQuery = nameOrKeyQuery;
+      return this;
+    }
+
+    public Builder setQualifiers(Collection<String> qualifiers) {
+      this.qualifiers = qualifiers;
+      return this;
+    }
+
+    public Builder setStrategy(Strategy strategy) {
+      this.strategy = requireNonNull(strategy);
+      return this;
+    }
+
+    /**
+     * All the measures are returned if parameter is {@code null}.
+     */
+    public Builder setMetricIds(@Nullable Collection<Integer> metricIds) {
+      this.metricIds = metricIds;
+      return this;
+    }
+
+    public Builder setPersonId(@Nullable Long personId) {
+      this.personId = personId;
+      return this;
+    }
+
+    public MeasureTreeQuery build() {
+      return new MeasureTreeQuery(this);
+    }
+  }
+}
index 8a3aa4973a944de71e73b2c9916b8816b4a7a965..4ca89c19d30b51b405d29cb1608c47b77dec99d8 100644 (file)
   <select id="selectDescendants" resultType="Component">
     select
     <include refid="componentColumns"/>
-    <include refid="selectDescendantsQuery"/>
-  </select>
-
-  <sql id="selectDescendantsQuery">
     from projects p
-    inner join projects base on base.project_uuid = p.project_uuid and base.uuid = #{query.baseUuid}
+    <include refid="selectDescendantsJoins"/>
     <where>
-      <choose>
-        <when test="query.getStrategy().name() == 'CHILDREN'">
-          and p.uuid_path = #{baseUuidPath}
-        </when>
-        <otherwise>
-          and p.uuid_path like #{baseUuidPath} ESCAPE '/'
-        </otherwise>
-      </choose>
       <include refid="selectDescendantsFilters"/>
     </where>
+  </select>
+
+  <sql id="selectDescendantsJoins">
+    inner join projects base on base.project_uuid = p.project_uuid and base.uuid = #{baseUuid}
+    <choose>
+      <when test="query.getStrategy().name() == 'CHILDREN'">
+        and p.uuid_path = #{baseUuidPath}
+      </when>
+      <otherwise>
+        and p.uuid_path like #{baseUuidPath} ESCAPE '/'
+      </otherwise>
+    </choose>
   </sql>
 
   <sql id="selectDescendantsFilters">
index 00534d151ebd89af94b7f09847be9b33eeb18bd2..61b1268b21968a658bd3b87e726dfb3a03ce8f13 100644 (file)
     </if>
     <choose>
       <when test="query.getPersonId() != null">
-        and person_id = #{query.personId}
+        and pm.person_id = #{query.personId}
       </when>
       <otherwise>
-        and person_id is null
+        and pm.person_id is null
       </otherwise>
     </choose>
   </sql>
 
+  <select id="selectTreeByQuery" parameterType="map" resultType="Measure">
+    select <include refid="measureColumns"/> from project_measures pm
+    inner join snapshots analysis on analysis.uuid = pm.analysis_uuid
+    inner join projects p on p.project_uuid=analysis.component_uuid and p.uuid=pm.component_uuid
+    <include refid="org.sonar.db.component.ComponentMapper.selectDescendantsJoins"/>
+    <where>
+      <include refid="selectTreeByQueryFilters"/>
+    </where>
+    -- Add measures of base component
+    union all
+    select <include refid="measureColumns"/> from project_measures pm
+    inner join snapshots analysis on analysis.uuid = pm.analysis_uuid
+    inner join projects p on p.project_uuid=analysis.component_uuid and p.uuid=pm.component_uuid and pm.component_uuid=#{baseUuid}
+    <where>
+      <include refid="selectTreeByQueryFilters"/>
+    </where>
+  </select>
+
+  <sql id="selectTreeByQueryFilters">
+    <if test="query.getMetricIds() != null">
+      and pm.metric_id in
+      <foreach item="metricId" collection="query.getMetricIds()" open="(" separator="," close=")">#{metricId}</foreach>
+    </if>
+    <choose>
+      <when test="query.getPersonId() != null">
+        and pm.person_id = #{query.personId}
+      </when>
+      <otherwise>
+        and pm.person_id is null
+      </otherwise>
+    </choose>
+    <include refid="org.sonar.db.component.ComponentMapper.selectDescendantsFilters"/>
+  </sql>
+
   <select id="selectPastMeasures" parameterType="map" resultType="org.sonar.db.measure.PastMeasureDto">
     select pm.id as id, pm.metric_id as metricId, pm.person_id as personId, pm.value as value
     from project_measures pm
index 70f3e206e232b60a90ae1dfa5156d069f878c6d7..a74b0460405a84c89d0baa655482f24b397acd66 100644 (file)
@@ -948,7 +948,7 @@ public class ComponentDaoTest {
   }
 
   @Test
-  public void select_descendants_with_leaves_stragegy() {
+  public void select_descendants_with_leaves_strategy() {
     ComponentDto project = newProjectDto(PROJECT_UUID);
     componentDb.insertProjectAndSnapshot(project);
     componentDb.insertComponent(newModuleDto("module-1-uuid", project));
index 44795f230a0d5b75899037d9315921e16a6ef487..405f6a5449d86d70e05175859a921b18e3ce480d 100644 (file)
@@ -23,42 +23,67 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 
-import static com.google.common.collect.Lists.newArrayList;
-import static java.util.Collections.singletonList;
+import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.component.ComponentTreeQuery.Strategy.CHILDREN;
+import static org.sonar.db.component.ComponentTreeQuery.Strategy.LEAVES;
 
 public class ComponentTreeQueryTest {
 
-  private static final String AN_UUID = "u1";
-
+  private static final String BASE_UUID = "ABCD";
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
   @Test
-  public void convert_sorts_in_sql_representation() {
-    ComponentTreeQuery result = ComponentTreeQuery.builder()
-      .setBaseUuid(AN_UUID)
-      .setSortFields(newArrayList("name", "path", "qualifier"))
+  public void create_query() throws Exception {
+    ComponentTreeQuery query = ComponentTreeQuery.builder()
+      .setBaseUuid(BASE_UUID)
+      .setStrategy(CHILDREN)
+      .setQualifiers(asList("FIL", "DIR"))
+      .setNameOrKeyQuery("teSt")
       .build();
 
-    assertThat(result.getSqlSort()).isEqualTo("LOWER(p.name) ASC, p.name ASC, LOWER(p.path) ASC, p.path ASC, LOWER(p.qualifier) ASC, p.qualifier ASC");
+    assertThat(query.getBaseUuid()).isEqualTo(BASE_UUID);
+    assertThat(query.getStrategy()).isEqualTo(CHILDREN);
+    assertThat(query.getQualifiers()).containsOnly("FIL", "DIR");
+    assertThat(query.getNameOrKeyQuery()).isEqualTo("teSt");
   }
 
   @Test
-  public void fail_if_no_base_uuid() {
-    expectedException.expect(NullPointerException.class);
+  public void create_minimal_query() throws Exception {
+    ComponentTreeQuery query = ComponentTreeQuery.builder()
+      .setBaseUuid(BASE_UUID)
+      .setStrategy(CHILDREN)
+      .build();
+
+    assertThat(query.getBaseUuid()).isEqualTo(BASE_UUID);
+    assertThat(query.getStrategy()).isEqualTo(CHILDREN);
+    assertThat(query.getQualifiers()).isNull();
+    assertThat(query.getNameOrKeyQuery()).isNull();
+  }
+
+  @Test
+  public void test_getUuidPath() throws Exception {
+    assertThat(ComponentTreeQuery.builder().setBaseUuid(BASE_UUID).setStrategy(CHILDREN)
+      .build().getUuidPath(ComponentTesting.newProjectDto("PROJECT_UUID"))).isEqualTo(".PROJECT_UUID.");
+
+    assertThat(ComponentTreeQuery.builder().setBaseUuid(BASE_UUID).setStrategy(LEAVES)
+      .build().getUuidPath(ComponentTesting.newProjectDto("PROJECT_UUID"))).isEqualTo(".PROJECT/_UUID.%");
+  }
 
+  @Test
+  public void fail_when_no_base_uuid() throws Exception {
+    expectedException.expect(NullPointerException.class);
     ComponentTreeQuery.builder()
-      .setSortFields(singletonList("name"))
+      .setStrategy(CHILDREN)
       .build();
   }
 
   @Test
-  public void fail_if_no_sort() {
+  public void fail_when_no_strategy() throws Exception {
     expectedException.expect(NullPointerException.class);
-
     ComponentTreeQuery.builder()
-      .setBaseUuid(AN_UUID)
+      .setBaseUuid(BASE_UUID)
       .build();
   }
 }
index af492d3666fd1130f674c0c31c5ca44bebbcc5a4..beddaf6c737367515ba8cbb8fcb90bac0cf2cd8a 100644 (file)
@@ -27,21 +27,26 @@ import javax.annotation.Nullable;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
-import org.sonar.api.resources.Qualifiers;
 import org.sonar.api.resources.Scopes;
 import org.sonar.api.utils.System2;
 import org.sonar.core.util.UuidFactoryImpl;
 import org.sonar.db.DbTester;
 import org.sonar.db.component.ComponentDto;
-import org.sonar.db.component.ComponentTesting;
 import org.sonar.db.component.SnapshotTesting;
 
 import static java.util.Arrays.asList;
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.resources.Qualifiers.FILE;
+import static org.sonar.api.resources.Qualifiers.PROJECT;
+import static org.sonar.api.resources.Qualifiers.UNIT_TEST_FILE;
+import static org.sonar.api.resources.Qualifiers.VIEW;
 import static org.sonar.db.component.ComponentTesting.newDeveloper;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
+import static org.sonar.db.component.ComponentTesting.newModuleDto;
+import static org.sonar.db.measure.MeasureTreeQuery.Strategy.CHILDREN;
+import static org.sonar.db.measure.MeasureTreeQuery.Strategy.LEAVES;
 
 public class MeasureDaoTest {
 
@@ -51,10 +56,11 @@ public class MeasureDaoTest {
   private static final long A_PERSON_ID = 444L;
   private static final String LAST_ANALYSIS_UUID = "A1";
   private static final String OTHER_ANALYSIS_UUID = "A2";
-  public static final String PREVIOUS_ANALYSIS_UUID = "previous analysis UUID";
+  private static final String PREVIOUS_ANALYSIS_UUID = "previous analysis UUID";
 
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
+
   @Rule
   public DbTester db = DbTester.create(System2.INSTANCE);
 
@@ -106,7 +112,7 @@ public class MeasureDaoTest {
   @Test
   public void selectByQuery() {
     ComponentDto project1 = db.components().insertProject();
-    ComponentDto module = db.components().insertComponent(ComponentTesting.newModuleDto(project1));
+    ComponentDto module = db.components().insertComponent(newModuleDto(project1));
     db.components().insertComponent(newFileDto(module).setUuid("C1"));
     db.components().insertComponent(newFileDto(module).setUuid("C2"));
     insertAnalysis(LAST_ANALYSIS_UUID, project1.uuid(), true);
@@ -201,7 +207,7 @@ public class MeasureDaoTest {
   @Test
   public void selectByQuery_with_handler() {
     ComponentDto project1 = db.components().insertProject();
-    ComponentDto module = db.components().insertComponent(ComponentTesting.newModuleDto(project1));
+    ComponentDto module = db.components().insertComponent(newModuleDto(project1));
     db.components().insertComponent(newFileDto(module).setUuid("C1"));
     db.components().insertComponent(newFileDto(module).setUuid("C2"));
     insertAnalysis(LAST_ANALYSIS_UUID, project1.uuid(), true);
@@ -325,9 +331,9 @@ public class MeasureDaoTest {
     long developerId = dev.getId();
     assertThat(underTest.selectProjectMeasuresOfDeveloper(db.getSession(), developerId, allMetricIds)).isEmpty();
 
-    String projectUuid = insertComponent(Scopes.PROJECT, Qualifiers.PROJECT, true);
-    String viewUuid = insertComponent(Scopes.PROJECT, Qualifiers.VIEW, true);
-    String disabledProjectUuid = insertComponent(Scopes.PROJECT, Qualifiers.PROJECT, false);
+    String projectUuid = insertComponent(Scopes.PROJECT, PROJECT, true);
+    String viewUuid = insertComponent(Scopes.PROJECT, VIEW, true);
+    String disabledProjectUuid = insertComponent(Scopes.PROJECT, PROJECT, false);
     insertMeasure("M1", LAST_ANALYSIS_UUID, projectUuid, NCLOC_METRIC_ID);
     insertMeasure("M2", LAST_ANALYSIS_UUID, projectUuid, COMPLEXITY_METRIC_ID);
     insertMeasure("M3", LAST_ANALYSIS_UUID, projectUuid, COVERAGE_METRIC_ID);
@@ -355,6 +361,58 @@ public class MeasureDaoTest {
       .containsOnly("M11", "M54");
   }
 
+  @Test
+  public void select_tree_by_query() {
+    ComponentDto project = db.components().insertProject();
+    ComponentDto module1 = db.components().insertComponent(newModuleDto(project));
+    ComponentDto module2 = db.components().insertComponent(newModuleDto(project));
+    ComponentDto file1 = db.components().insertComponent(newFileDto(module1).setUuid("C1").setName("File One"));
+    db.components().insertComponent(newFileDto(module2).setUuid("C2").setName("File Two").setQualifier(UNIT_TEST_FILE));
+    insertAnalysis(LAST_ANALYSIS_UUID, project.uuid(), true);
+    db.components().indexAllComponents();
+
+    // project
+    insertMeasure("PROJECT_M1", LAST_ANALYSIS_UUID, project.uuid(), NCLOC_METRIC_ID);
+    // module 1
+    insertMeasure("MODULE_M1", LAST_ANALYSIS_UUID, module1.uuid(), NCLOC_METRIC_ID);
+    // component C1
+    insertMeasure("M2", LAST_ANALYSIS_UUID, "C1", NCLOC_METRIC_ID);
+    insertMeasure("M3", LAST_ANALYSIS_UUID, "C1", COVERAGE_METRIC_ID);
+    insertMeasureOnPerson("M4", LAST_ANALYSIS_UUID, "C1", NCLOC_METRIC_ID, A_PERSON_ID);
+    // component C2
+    insertMeasure("M6", LAST_ANALYSIS_UUID, "C2", NCLOC_METRIC_ID);
+    db.commit();
+
+    // Children measures of project
+    verifyMeasures(project, MeasureTreeQuery.builder().setStrategy(CHILDREN), "PROJECT_M1", "MODULE_M1");
+
+    // Children measures of module 1
+    verifyMeasures(module1, MeasureTreeQuery.builder().setStrategy(CHILDREN), "M2", "M3", "MODULE_M1");
+
+    // Children measure on file => only measures from itself
+    verifyMeasures(file1, MeasureTreeQuery.builder().setStrategy(CHILDREN), "M2", "M3");
+
+    // Leaves measures of project
+    verifyMeasures(project, MeasureTreeQuery.builder().setStrategy(LEAVES), "PROJECT_M1", "MODULE_M1", "M2", "M3", "M6");
+
+    // Leaves measures of module 1
+    verifyMeasures(module1, MeasureTreeQuery.builder().setStrategy(LEAVES), "MODULE_M1", "M2", "M3");
+
+    // Leaves measures of project by metric ids
+    verifyMeasures(project, MeasureTreeQuery.builder().setMetricIds(asList(NCLOC_METRIC_ID)).setStrategy(LEAVES), "PROJECT_M1", "MODULE_M1", "M2",
+      "M6");
+
+    // Leaves measure on file
+    verifyMeasures(file1, MeasureTreeQuery.builder().setStrategy(LEAVES), "M2", "M3");
+
+    // Leaves measures of project matching name
+    verifyMeasures(project, MeasureTreeQuery.builder().setNameOrKeyQuery("OnE").setStrategy(LEAVES), "M2", "M3");
+
+    // Leaves measures of project matching qualifiers
+    verifyMeasures(project, MeasureTreeQuery.builder().setQualifiers(asList(FILE)).setStrategy(LEAVES), "M2", "M3");
+    verifyMeasures(project, MeasureTreeQuery.builder().setQualifiers(asList(FILE, UNIT_TEST_FILE)).setStrategy(LEAVES), "M2", "M3", "M6");
+  }
+
   private Optional<MeasureDto> selectSingle(MeasureQuery.Builder query) {
     return underTest.selectSingle(db.getSession(), query.build());
   }
@@ -378,6 +436,16 @@ public class MeasureDaoTest {
     assertThat(measures).isEmpty();
   }
 
+  private void verifyMeasures(ComponentDto baseComponent, MeasureTreeQuery.Builder measureQuery, String... expectedIds) {
+    assertThat(underTest.selectTreeByQuery(db.getSession(), baseComponent, measureQuery.build()))
+      .extracting(MeasureDto::getData).containsOnly(expectedIds);
+  }
+
+  private void verifyZeroMeasures(ComponentDto baseComponent, MeasureTreeQuery.Builder measureQuery) {
+    assertThat(underTest.selectTreeByQuery(db.getSession(), baseComponent,
+      measureQuery.build())).isEmpty();
+  }
+
   private List<MeasureDto> getMeasuresWithHandler(MeasureQuery.Builder query) {
     List<MeasureDto> measures = new ArrayList<>();
     underTest.selectByQuery(db.getSession(), query.build(), resultContext -> measures.add((MeasureDto) resultContext.getResultObject()));
diff --git a/sonar-db/src/test/java/org/sonar/db/measure/MeasureTreeQueryTest.java b/sonar-db/src/test/java/org/sonar/db/measure/MeasureTreeQueryTest.java
new file mode 100644 (file)
index 0000000..9975ac9
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.db.measure;
+
+import java.util.Collections;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.db.component.ComponentTesting;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.measure.MeasureTreeQuery.Strategy.CHILDREN;
+import static org.sonar.db.measure.MeasureTreeQuery.Strategy.LEAVES;
+
+public class MeasureTreeQueryTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void create_query() throws Exception {
+    MeasureTreeQuery query = MeasureTreeQuery.builder()
+      .setStrategy(CHILDREN)
+      .setQualifiers(asList("FIL", "DIR"))
+      .setNameOrKeyQuery("teSt")
+      .setMetricIds(asList(10, 11))
+      .setPersonId(100L)
+      .build();
+
+    assertThat(query.getStrategy()).isEqualTo(CHILDREN);
+    assertThat(query.getQualifiers()).containsOnly("FIL", "DIR");
+    assertThat(query.getNameOrKeyQuery()).isEqualTo("teSt");
+    assertThat(query.getMetricIds()).containsOnly(10, 11);
+    assertThat(query.getPersonId()).isEqualTo(100L);
+  }
+
+  @Test
+  public void create_minimal_query() throws Exception {
+    MeasureTreeQuery query = MeasureTreeQuery.builder()
+      .setStrategy(CHILDREN)
+      .build();
+
+    assertThat(query.getStrategy()).isEqualTo(CHILDREN);
+    assertThat(query.getQualifiers()).isNull();
+    assertThat(query.getNameOrKeyQuery()).isNull();
+    assertThat(query.getMetricIds()).isNull();
+    assertThat(query.getPersonId()).isNull();
+  }
+
+  @Test
+  public void test_getNameOrKeyQueryToSqlForResourceIndex() throws Exception {
+    assertThat(MeasureTreeQuery.builder()
+      .setNameOrKeyQuery("like-\\_%/-value")
+      .setStrategy(CHILDREN)
+      .build().getNameOrKeyQueryToSqlForResourceIndex()).isEqualTo("like-\\/_/%//-value%");
+
+    assertThat(MeasureTreeQuery.builder()
+      .setStrategy(CHILDREN)
+      .build().getNameOrKeyQueryToSqlForResourceIndex()).isNull();
+  }
+
+  @Test
+  public void test_getUuidPath() throws Exception {
+    assertThat(MeasureTreeQuery.builder().setStrategy(CHILDREN)
+      .build().getUuidPath(ComponentTesting.newProjectDto("PROJECT_UUID"))).isEqualTo(".PROJECT_UUID.");
+
+    assertThat(MeasureTreeQuery.builder().setStrategy(LEAVES)
+      .build().getUuidPath(ComponentTesting.newProjectDto("PROJECT_UUID"))).isEqualTo(".PROJECT/_UUID.%");
+  }
+
+  @Test
+  public void return_empty_when_metrics_is_empty() throws Exception {
+    assertThat(MeasureTreeQuery.builder()
+      .setStrategy(CHILDREN)
+      .setMetricIds(Collections.emptyList())
+      .build().returnsEmpty()).isTrue();
+
+    assertThat(MeasureTreeQuery.builder()
+      .setStrategy(CHILDREN)
+      .setMetricIds(null)
+      .build().returnsEmpty()).isFalse();
+  }
+
+  @Test
+  public void return_empty_when_qualifiers_is_empty() throws Exception {
+    assertThat(MeasureTreeQuery.builder()
+      .setStrategy(CHILDREN)
+      .setQualifiers(Collections.emptyList())
+      .build().returnsEmpty()).isTrue();
+
+    assertThat(MeasureTreeQuery.builder()
+      .setStrategy(CHILDREN)
+      .setQualifiers(asList("FIL", "DIR"))
+      .build().returnsEmpty()).isFalse();
+  }
+
+  @Test
+  public void fail_when_no_strategy() throws Exception {
+    expectedException.expect(NullPointerException.class);
+    MeasureTreeQuery.builder()
+      .build();
+  }
+
+}