]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11904 Create API endpoint to get snippets
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Wed, 17 Apr 2019 12:57:26 +0000 (07:57 -0500)
committersonartech <sonartech@sonarsource.com>
Mon, 6 May 2019 09:01:15 +0000 (11:01 +0200)
18 files changed:
server/sonar-db-dao/src/test/java/org/sonar/db/source/FileSourceTester.java
server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java
server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentViewerJsonWriter.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/source/SourceService.java
server/sonar-server/src/main/java/org/sonar/server/source/ws/IssueSnippetsAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java
server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesJsonWriter.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/source/ws/ShowAction.java
server/sonar-server/src/test/java/org/sonar/server/component/ws/AppActionTest.java
server/sonar-server/src/test/java/org/sonar/server/source/SourceServiceTest.java
server/sonar-server/src/test/java/org/sonar/server/source/ws/IssueSnippetsActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java
server/sonar-server/src/test/java/org/sonar/server/source/ws/SourcesWsTest.java
server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_close_to_each_other.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_multiple_locations.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_single_location.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_with_measures.json [new file with mode: 0644]

index c829c5cdc80652eeff9f5133a79bba91f3470b8d..aefcca8aada5c4503d0fbb8dbf9837c1540f83da 100644 (file)
@@ -58,6 +58,24 @@ public class FileSourceTester {
     return dto;
   }
 
+  @SafeVarargs
+  public final FileSourceDto insertFileSource(ComponentDto file, int numLines, Consumer<FileSourceDto>... dtoPopulators) {
+    FileSourceDto dto = new FileSourceDto()
+      .setProjectUuid(file.projectUuid())
+      .setFileUuid(file.uuid())
+      .setSrcHash(randomAlphanumeric(50))
+      .setDataHash(randomAlphanumeric(50))
+      .setLineHashes(IntStream.range(0, numLines).mapToObj(String::valueOf).collect(MoreCollectors.toList()))
+      .setRevision(randomAlphanumeric(100))
+      .setSourceData(newRandomData(numLines).build())
+      .setCreatedAt(new Date().getTime())
+      .setUpdatedAt(new Date().getTime());
+    Arrays.stream(dtoPopulators).forEach(c -> c.accept(dto));
+    db.getDbClient().fileSourceDao().insert(db.getSession(), dto);
+    db.commit();
+    return dto;
+  }
+
   private static DbFileSources.Data.Builder newRandomData(int numberOfLines) {
     DbFileSources.Data.Builder dataBuilder = DbFileSources.Data.newBuilder();
     for (int i = 1; i <= numberOfLines; i++) {
index 2a9a01b76e5bc8b689691a27bf7558dd65a609f0..e103e19e1b9d9736c234e66eecbc3db9c51aa303 100644 (file)
  */
 package org.sonar.server.component.ws;
 
-import com.google.common.collect.Maps;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-import org.apache.commons.lang.BooleanUtils;
-import org.sonar.api.measures.Metric;
 import org.sonar.api.server.ws.Change;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
@@ -36,26 +28,10 @@ import org.sonar.api.web.UserRole;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
-import org.sonar.db.measure.LiveMeasureDto;
-import org.sonar.db.metric.MetricDto;
-import org.sonar.db.property.PropertyDto;
-import org.sonar.db.property.PropertyQuery;
 import org.sonar.server.component.ComponentFinder;
 import org.sonar.server.user.UserSession;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static java.util.Arrays.asList;
-import static java.util.Collections.unmodifiableList;
-import static org.sonar.api.measures.CoreMetrics.COVERAGE;
-import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY;
-import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY;
-import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY_KEY;
-import static org.sonar.api.measures.CoreMetrics.LINES;
-import static org.sonar.api.measures.CoreMetrics.LINES_KEY;
-import static org.sonar.api.measures.CoreMetrics.TESTS;
-import static org.sonar.api.measures.CoreMetrics.TESTS_KEY;
-import static org.sonar.api.measures.CoreMetrics.VIOLATIONS;
-import static org.sonar.api.measures.CoreMetrics.VIOLATIONS_KEY;
 import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01;
 import static org.sonar.server.component.ComponentFinder.ParamNames.COMPONENT_ID_AND_COMPONENT;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_BRANCH;
@@ -65,25 +41,20 @@ import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 
 public class AppAction implements ComponentsWsAction {
-
   private static final String PARAM_COMPONENT_ID = "componentId";
   private static final String PARAM_COMPONENT = "component";
-  private static final List<String> METRIC_KEYS = unmodifiableList(asList(
-    LINES_KEY,
-    VIOLATIONS_KEY,
-    COVERAGE_KEY,
-    DUPLICATED_LINES_DENSITY_KEY,
-    TESTS_KEY));
 
   private final DbClient dbClient;
 
   private final UserSession userSession;
   private final ComponentFinder componentFinder;
+  private final ComponentViewerJsonWriter componentViewerJsonWriter;
 
-  public AppAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder) {
+  public AppAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder, ComponentViewerJsonWriter componentViewerJsonWriter) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.componentFinder = componentFinder;
+    this.componentViewerJsonWriter = componentViewerJsonWriter;
   }
 
   @Override
@@ -130,10 +101,9 @@ public class AppAction implements ComponentsWsAction {
 
       JsonWriter json = response.newJsonWriter();
       json.beginObject();
-      Map<String, LiveMeasureDto> measuresByMetricKey = loadMeasuresGroupedByMetricKey(component, session);
-      appendComponent(json, component, userSession, session);
+      componentViewerJsonWriter.writeComponent(json, component, userSession, session);
       appendPermissions(json, userSession);
-      appendMeasures(json, measuresByMetricKey);
+      componentViewerJsonWriter.writeMeasures(json, component, session);
       json.endObject();
       json.close();
     }
@@ -151,97 +121,8 @@ public class AppAction implements ComponentsWsAction {
     return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, request.mandatoryParam(PARAM_COMPONENT), branch, pullRequest);
   }
 
-  private void appendComponent(JsonWriter json, ComponentDto component, UserSession userSession, DbSession session) {
-    List<PropertyDto> propertyDtos = dbClient.propertiesDao().selectByQuery(PropertyQuery.builder()
-      .setKey("favourite")
-      .setComponentId(component.getId())
-      .setUserId(userSession.getUserId())
-      .build(),
-      session);
-    boolean isFavourite = propertyDtos.size() == 1;
-
-    json.prop("key", component.getKey());
-    json.prop("uuid", component.uuid());
-    json.prop("path", component.path());
-    json.prop("name", component.name());
-    json.prop("longName", component.longName());
-    json.prop("q", component.qualifier());
-
-    ComponentDto parentModule = retrieveParentModuleIfNotCurrentComponent(component, session);
-    ComponentDto project = dbClient.componentDao().selectOrFailByUuid(session, component.projectUuid());
-
-    // Do not display parent module if parent module and project are the same
-    boolean displayParentModule = parentModule != null && !parentModule.uuid().equals(project.uuid());
-    json.prop("subProject", displayParentModule ? parentModule.getKey() : null);
-    json.prop("subProjectName", displayParentModule ? parentModule.longName() : null);
-    json.prop("project", project.getKey());
-    json.prop("projectName", project.longName());
-    String branch = project.getBranch();
-    if (branch != null) {
-      json.prop("branch", branch);
-    }
-    String pullRequest = project.getPullRequest();
-    if (pullRequest != null) {
-      json.prop("pullRequest", pullRequest);
-    }
-
-    json.prop("fav", isFavourite);
-  }
-
   private static void appendPermissions(JsonWriter json, UserSession userSession) {
     json.prop("canMarkAsFavorite", userSession.isLoggedIn());
   }
 
-  private static void appendMeasures(JsonWriter json, Map<String, LiveMeasureDto> measuresByMetricKey) {
-    json.name("measures").beginObject();
-    json.prop("lines", formatMeasure(measuresByMetricKey, LINES));
-    json.prop("coverage", formatMeasure(measuresByMetricKey, COVERAGE));
-    json.prop("duplicationDensity", formatMeasure(measuresByMetricKey, DUPLICATED_LINES_DENSITY));
-    json.prop("issues", formatMeasure(measuresByMetricKey, VIOLATIONS));
-    json.prop("tests", formatMeasure(measuresByMetricKey, TESTS));
-    json.endObject();
-  }
-
-  private Map<String, LiveMeasureDto> loadMeasuresGroupedByMetricKey(ComponentDto component, DbSession dbSession) {
-    List<MetricDto> metrics = dbClient.metricDao().selectByKeys(dbSession, METRIC_KEYS);
-    Map<Integer, MetricDto> metricsById = Maps.uniqueIndex(metrics, MetricDto::getId);
-    List<LiveMeasureDto> measures = dbClient.liveMeasureDao()
-      .selectByComponentUuidsAndMetricIds(dbSession, Collections.singletonList(component.uuid()), metricsById.keySet());
-    return Maps.uniqueIndex(measures, m -> metricsById.get(m.getMetricId()).getKey());
-  }
-
-  @CheckForNull
-  private ComponentDto retrieveParentModuleIfNotCurrentComponent(ComponentDto componentDto, DbSession session) {
-    final String moduleUuid = componentDto.moduleUuid();
-    if (moduleUuid == null || componentDto.uuid().equals(moduleUuid)) {
-      return null;
-    }
-    return dbClient.componentDao().selectOrFailByUuid(session, moduleUuid);
-  }
-
-  @CheckForNull
-  private static String formatMeasure(Map<String, LiveMeasureDto> measuresByMetricKey, Metric metric) {
-    LiveMeasureDto measure = measuresByMetricKey.get(metric.getKey());
-    return formatMeasure(measure, metric);
-  }
-
-  private static String formatMeasure(@Nullable LiveMeasureDto measure, Metric metric) {
-    if (measure == null) {
-      return null;
-    }
-    Double value = getDoubleValue(measure, metric);
-    if (value != null) {
-      return Double.toString(value);
-    }
-    return null;
-  }
-
-  @CheckForNull
-  private static Double getDoubleValue(LiveMeasureDto measure, Metric metric) {
-    Double value = measure.getValue();
-    if (BooleanUtils.isTrue(metric.isOptimizedBestValue()) && value == null) {
-      value = metric.getBestValue();
-    }
-    return value;
-  }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentViewerJsonWriter.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentViewerJsonWriter.java
new file mode 100644 (file)
index 0000000..4ea0b0e
--- /dev/null
@@ -0,0 +1,164 @@
+/*
+ * 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.component.ws;
+
+import com.google.common.collect.Maps;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.BooleanUtils;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.measure.LiveMeasureDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.db.property.PropertyQuery;
+import org.sonar.server.user.UserSession;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.unmodifiableList;
+import static org.sonar.api.measures.CoreMetrics.COVERAGE;
+import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY;
+import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY;
+import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY_KEY;
+import static org.sonar.api.measures.CoreMetrics.LINES;
+import static org.sonar.api.measures.CoreMetrics.LINES_KEY;
+import static org.sonar.api.measures.CoreMetrics.TESTS;
+import static org.sonar.api.measures.CoreMetrics.TESTS_KEY;
+import static org.sonar.api.measures.CoreMetrics.VIOLATIONS;
+import static org.sonar.api.measures.CoreMetrics.VIOLATIONS_KEY;
+
+public class ComponentViewerJsonWriter {
+  private static final List<String> METRIC_KEYS = unmodifiableList(asList(
+    LINES_KEY,
+    VIOLATIONS_KEY,
+    COVERAGE_KEY,
+    DUPLICATED_LINES_DENSITY_KEY,
+    TESTS_KEY));
+
+  private final DbClient dbClient;
+
+  public ComponentViewerJsonWriter(DbClient dbClient) {
+    this.dbClient = dbClient;
+  }
+
+  public void writeComponentWithoutFav(JsonWriter json, ComponentDto component, DbSession session, boolean includeSubProject) {
+    json.prop("key", component.getKey());
+    json.prop("uuid", component.uuid());
+    json.prop("path", component.path());
+    json.prop("name", component.name());
+    json.prop("longName", component.longName());
+    json.prop("q", component.qualifier());
+
+    ComponentDto project = dbClient.componentDao().selectOrFailByUuid(session, component.projectUuid());
+
+    if (includeSubProject) {
+      ComponentDto parentModule = retrieveParentModuleIfNotCurrentComponent(component, session);
+
+      // Do not display parent module if parent module and project are the same
+      boolean displayParentModule = parentModule != null && !parentModule.uuid().equals(project.uuid());
+      json.prop("subProject", displayParentModule ? parentModule.getKey() : null);
+      json.prop("subProjectName", displayParentModule ? parentModule.longName() : null);
+    }
+    json.prop("project", project.getKey());
+    json.prop("projectName", project.longName());
+    String branch = project.getBranch();
+    if (branch != null) {
+      json.prop("branch", branch);
+    }
+    String pullRequest = project.getPullRequest();
+    if (pullRequest != null) {
+      json.prop("pullRequest", pullRequest);
+    }
+  }
+
+  public void writeComponent(JsonWriter json, ComponentDto component, UserSession userSession, DbSession session) {
+    writeComponentWithoutFav(json, component, session, true);
+
+    List<PropertyDto> propertyDtos = dbClient.propertiesDao().selectByQuery(PropertyQuery.builder()
+        .setKey("favourite")
+        .setComponentId(component.getId())
+        .setUserId(userSession.getUserId())
+        .build(),
+      session);
+    boolean isFavourite = propertyDtos.size() == 1;
+    json.prop("fav", isFavourite);
+  }
+
+  public void writeMeasures(JsonWriter json, ComponentDto component, DbSession session) {
+    Map<String, LiveMeasureDto> measuresByMetricKey = loadMeasuresGroupedByMetricKey(component, session);
+
+    json.name("measures").beginObject();
+    json.prop("lines", formatMeasure(measuresByMetricKey, LINES));
+    json.prop("coverage", formatMeasure(measuresByMetricKey, COVERAGE));
+    json.prop("duplicationDensity", formatMeasure(measuresByMetricKey, DUPLICATED_LINES_DENSITY));
+    json.prop("issues", formatMeasure(measuresByMetricKey, VIOLATIONS));
+    json.prop("tests", formatMeasure(measuresByMetricKey, TESTS));
+    json.endObject();
+  }
+
+  private Map<String, LiveMeasureDto> loadMeasuresGroupedByMetricKey(ComponentDto component, DbSession dbSession) {
+    List<MetricDto> metrics = dbClient.metricDao().selectByKeys(dbSession, METRIC_KEYS);
+    Map<Integer, MetricDto> metricsById = Maps.uniqueIndex(metrics, MetricDto::getId);
+    List<LiveMeasureDto> measures = dbClient.liveMeasureDao()
+      .selectByComponentUuidsAndMetricIds(dbSession, Collections.singletonList(component.uuid()), metricsById.keySet());
+    return Maps.uniqueIndex(measures, m -> metricsById.get(m.getMetricId()).getKey());
+  }
+
+  @CheckForNull
+  private static String formatMeasure(Map<String, LiveMeasureDto> measuresByMetricKey, Metric metric) {
+    LiveMeasureDto measure = measuresByMetricKey.get(metric.getKey());
+    return formatMeasure(measure, metric);
+  }
+
+  private static String formatMeasure(@Nullable LiveMeasureDto measure, Metric metric) {
+    if (measure == null) {
+      return null;
+    }
+    Double value = getDoubleValue(measure, metric);
+    if (value != null) {
+      return Double.toString(value);
+    }
+    return null;
+  }
+
+  @CheckForNull
+  private static Double getDoubleValue(LiveMeasureDto measure, Metric metric) {
+    Double value = measure.getValue();
+    if (BooleanUtils.isTrue(metric.isOptimizedBestValue()) && value == null) {
+      value = metric.getBestValue();
+    }
+    return value;
+  }
+
+  @CheckForNull
+  private ComponentDto retrieveParentModuleIfNotCurrentComponent(ComponentDto componentDto, DbSession session) {
+    final String moduleUuid = componentDto.moduleUuid();
+    if (moduleUuid == null || componentDto.uuid().equals(moduleUuid)) {
+      return null;
+    }
+    return dbClient.componentDao().selectOrFailByUuid(session, moduleUuid);
+  }
+}
index a2e039daa3b09b5583ce8f5e55d24019138ac398..1ebaef947d4019011ed7c752be78b58135f55c25 100644 (file)
@@ -49,6 +49,7 @@ import org.sonar.server.component.ComponentUpdater;
 import org.sonar.server.component.index.ComponentIndex;
 import org.sonar.server.component.index.ComponentIndexDefinition;
 import org.sonar.server.component.index.ComponentIndexer;
+import org.sonar.server.component.ws.ComponentViewerJsonWriter;
 import org.sonar.server.component.ws.ComponentsWsModule;
 import org.sonar.server.debt.DebtModelPluginRepository;
 import org.sonar.server.debt.DebtModelXMLExporter;
@@ -186,7 +187,9 @@ import org.sonar.server.source.HtmlSourceDecorator;
 import org.sonar.server.source.SourceService;
 import org.sonar.server.source.ws.HashAction;
 import org.sonar.server.source.ws.IndexAction;
+import org.sonar.server.source.ws.IssueSnippetsAction;
 import org.sonar.server.source.ws.LinesAction;
+import org.sonar.server.source.ws.LinesJsonWriter;
 import org.sonar.server.source.ws.RawAction;
 import org.sonar.server.source.ws.ScmAction;
 import org.sonar.server.source.ws.SourcesWs;
@@ -390,6 +393,7 @@ public class PlatformLevel4 extends PlatformLevel {
       ComponentIndex.class,
       ComponentIndexer.class,
       LiveMeasureModule.class,
+      ComponentViewerJsonWriter.class,
 
       FavoriteModule.class,
 
@@ -432,9 +436,11 @@ public class PlatformLevel4 extends PlatformLevel {
 
       // source
       HtmlSourceDecorator.class,
+      LinesJsonWriter.class,
       SourceService.class,
       SourcesWs.class,
       org.sonar.server.source.ws.ShowAction.class,
+      IssueSnippetsAction.class,
       LinesAction.class,
       HashAction.class,
       RawAction.class,
index 7d166a390ffb35d046b0749887481931876c8615..44a3013179df68231eac33cd3c84713d35d5ea91 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.server.source;
 
 import java.util.Optional;
+import java.util.Set;
 import java.util.function.Function;
 import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
@@ -43,15 +44,21 @@ public class SourceService {
 
   /**
    * Returns a range of lines as raw db data. User permission is not verified.
-   * @param from starts from 1
+   *
+   * @param from        starts from 1
    * @param toInclusive starts from 1, must be greater than or equal param {@code from}
    */
   public Optional<Iterable<DbFileSources.Line>> getLines(DbSession dbSession, String fileUuid, int from, int toInclusive) {
     return getLines(dbSession, fileUuid, from, toInclusive, Function.identity());
   }
 
+  public Optional<Iterable<DbFileSources.Line>> getLines(DbSession dbSession, String fileUuid, Set<Integer> lines) {
+    return getLines(dbSession, fileUuid, lines, Function.identity());
+  }
+
   /**
    * Returns a range of lines as raw text.
+   *
    * @see #getLines(DbSession, String, int, int)
    */
   public Optional<Iterable<String>> getLinesAsRawText(DbSession dbSession, String fileUuid, int from, int toInclusive) {
@@ -76,6 +83,17 @@ public class SourceService {
       .collect(MoreCollectors.toList()));
   }
 
+  private <E> Optional<Iterable<E>> getLines(DbSession dbSession, String fileUuid, Set<Integer> lines, Function<DbFileSources.Line, E> function) {
+    FileSourceDto dto = dbClient.fileSourceDao().selectByFileUuid(dbSession, fileUuid);
+    if (dto == null) {
+      return Optional.empty();
+    }
+    return Optional.of(dto.getSourceData().getLinesList().stream()
+      .filter(line -> line.hasLine() && lines.contains(line.getLine()))
+      .map(function)
+      .collect(MoreCollectors.toList()));
+  }
+
   private static void verifyLine(int line) {
     checkArgument(line >= 1, String.format("Line number must start at 1, got %d", line));
   }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/source/ws/IssueSnippetsAction.java b/server/sonar-server/src/main/java/org/sonar/server/source/ws/IssueSnippetsAction.java
new file mode 100644 (file)
index 0000000..35c491a
--- /dev/null
@@ -0,0 +1,178 @@
+/*
+ * 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.source.ws;
+
+import com.google.common.io.Resources;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.protobuf.DbCommons;
+import org.sonar.db.protobuf.DbFileSources;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.server.component.ws.ComponentViewerJsonWriter;
+import org.sonar.server.issue.IssueFinder;
+import org.sonar.server.source.SourceService;
+import org.sonar.server.user.UserSession;
+
+public class IssueSnippetsAction implements SourcesWsAction {
+  private final IssueFinder issueFinder;
+  private final LinesJsonWriter linesJsonWriter;
+  private final ComponentViewerJsonWriter componentViewerJsonWriter;
+  private final SourceService sourceService;
+  private final DbClient dbClient;
+
+  public IssueSnippetsAction(SourceService sourceService, DbClient dbClient, IssueFinder issueFinder, LinesJsonWriter linesJsonWriter,
+    ComponentViewerJsonWriter componentViewerJsonWriter) {
+    this.sourceService = sourceService;
+    this.dbClient = dbClient;
+    this.issueFinder = issueFinder;
+    this.linesJsonWriter = linesJsonWriter;
+    this.componentViewerJsonWriter = componentViewerJsonWriter;
+  }
+
+  @Override
+  public void define(WebService.NewController controller) {
+    WebService.NewAction action = controller.createAction("issue_snippets")
+      .setDescription("Get code snipets involved in an issue. Requires 'Browse' permission on the project<br/>")
+      .setSince("7.8")
+      .setInternal(true)
+      .setResponseExample(Resources.getResource(getClass(), "example-show.json"))
+      .setHandler(this);
+
+    action
+      .createParam("issueKey")
+      .setRequired(true)
+      .setDescription("Issue key")
+      .setExampleValue("AU-Tpxb--iU5OvuD2FLy");
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    String issueKey = request.mandatoryParam("issueKey");
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      IssueDto issueDto = issueFinder.getByKey(dbSession, issueKey);
+
+      Map<String, TreeSet<Integer>> linesPerComponent;
+      Map<String, ComponentDto> componentsByUuid;
+
+      DbIssues.Locations locations = issueDto.parseLocations();
+      if (locations != null && issueDto.getComponentUuid() != null) {
+        linesPerComponent = getLinesPerComponent(issueDto.getComponentUuid(), locations);
+        componentsByUuid = dbClient.componentDao().selectByUuids(dbSession, linesPerComponent.keySet())
+          .stream().collect(Collectors.toMap(ComponentDto::uuid, c -> c));
+      } else {
+        componentsByUuid = Collections.emptyMap();
+        linesPerComponent = Collections.emptyMap();
+      }
+
+      try (JsonWriter jsonWriter = response.newJsonWriter()) {
+        jsonWriter.beginObject();
+
+        for (Map.Entry<String, TreeSet<Integer>> e : linesPerComponent.entrySet()) {
+          ComponentDto componentDto = componentsByUuid.get(e.getKey());
+          if (componentDto != null) {
+            processComponent(dbSession, jsonWriter, componentDto, e.getKey(), e.getValue());
+          }
+        }
+
+        jsonWriter.endObject();
+      }
+    }
+  }
+
+  private void processComponent(DbSession dbSession, JsonWriter writer, ComponentDto componentDto, String fileUuid, Set<Integer> lines) {
+    Optional<Iterable<DbFileSources.Line>> lineSourcesOpt = sourceService.getLines(dbSession, fileUuid, lines);
+    if (!lineSourcesOpt.isPresent()) {
+      return;
+    }
+
+    Supplier<Optional<Long>> periodDateSupplier = () -> dbClient.snapshotDao()
+      .selectLastAnalysisByComponentUuid(dbSession, componentDto.projectUuid())
+      .map(SnapshotDto::getPeriodDate);
+
+    Iterable<DbFileSources.Line> lineSources = lineSourcesOpt.get();
+
+    writer.name(componentDto.getKey()).beginObject();
+
+    writer.name("component").beginObject();
+    componentViewerJsonWriter.writeComponentWithoutFav(writer, componentDto, dbSession, false);
+    componentViewerJsonWriter.writeMeasures(writer, componentDto, dbSession);
+    writer.endObject();
+    linesJsonWriter.writeSource(lineSources, writer, true, periodDateSupplier);
+
+    writer.endObject();
+  }
+
+  private Map<String, TreeSet<Integer>> getLinesPerComponent(String componentUuid, DbIssues.Locations locations) {
+    Map<String, TreeSet<Integer>> linesPerComponent = new HashMap<>();
+
+    if (locations.hasTextRange()) {
+      // extra lines for the main location
+      addTextRange(linesPerComponent, componentUuid, locations.getTextRange(), 9);
+    }
+    for (DbIssues.Flow flow : locations.getFlowList()) {
+      for (DbIssues.Location l : flow.getLocationList()) {
+        if (l.hasComponentId()) {
+          addTextRange(linesPerComponent, l.getComponentId(), l.getTextRange(), 2);
+        } else {
+          addTextRange(linesPerComponent, componentUuid, l.getTextRange(), 2);
+        }
+      }
+    }
+
+    return linesPerComponent;
+  }
+
+  private static void addTextRange(Map<String, TreeSet<Integer>> linesPerComponent, String componentUuid,
+    DbCommons.TextRange textRange, int numLinesAfterIssue) {
+    int start = textRange.getStartLine() - 2;
+    int end = textRange.getEndLine() + numLinesAfterIssue;
+
+    TreeSet<Integer> lines = linesPerComponent.computeIfAbsent(componentUuid, c -> new TreeSet<>());
+    IntStream.rangeClosed(start, end).forEach(lines::add);
+
+    // If two snippets in the same component are 3 lines apart of each other, include those 3 lines.
+    Integer closestToStart = lines.lower(start);
+    if (closestToStart != null && closestToStart >= start - 4) {
+      IntStream.range(closestToStart + 1, start).forEach(lines::add);
+    }
+
+    Integer closestToEnd = lines.higher(end);
+    if (closestToEnd != null && closestToEnd <= end + 4) {
+      IntStream.range(end + 1, closestToEnd).forEach(lines::add);
+    }
+
+  }
+}
index fd6ce0c9aee73cec378d250f650947b88a4d2250..eaa21aa709704bd4d209db1944b06bb2f053a19b 100644 (file)
@@ -21,14 +21,12 @@ package org.sonar.server.source.ws;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.io.Resources;
-import java.util.Date;
 import java.util.Optional;
 import java.util.function.Supplier;
 import org.sonar.api.server.ws.Change;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
-import org.sonar.api.utils.DateUtils;
 import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.api.web.UserRole;
 import org.sonar.db.DbClient;
@@ -38,7 +36,6 @@ import org.sonar.db.component.SnapshotDto;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.protobuf.DbFileSources;
 import org.sonar.server.component.ComponentFinder;
-import org.sonar.server.source.HtmlSourceDecorator;
 import org.sonar.server.source.SourceService;
 import org.sonar.server.user.UserSession;
 
@@ -61,15 +58,15 @@ public class LinesAction implements SourcesWsAction {
 
   private final ComponentFinder componentFinder;
   private final SourceService sourceService;
-  private final HtmlSourceDecorator htmlSourceDecorator;
+  private final LinesJsonWriter linesJsonWriter;
   private final DbClient dbClient;
   private final UserSession userSession;
 
   public LinesAction(ComponentFinder componentFinder, DbClient dbClient, SourceService sourceService,
-    HtmlSourceDecorator htmlSourceDecorator, UserSession userSession) {
+    LinesJsonWriter linesJsonWriter, UserSession userSession) {
     this.componentFinder = componentFinder;
     this.sourceService = sourceService;
-    this.htmlSourceDecorator = htmlSourceDecorator;
+    this.linesJsonWriter = linesJsonWriter;
     this.dbClient = dbClient;
     this.userSession = userSession;
   }
@@ -77,7 +74,7 @@ public class LinesAction implements SourcesWsAction {
   @Override
   public void define(WebService.NewController controller) {
     WebService.NewAction action = controller.createAction("lines")
-      .setDescription("Show source code with line oriented info. Require See Source Code permission on file's project<br/>" +
+      .setDescription("Show source code with line oriented info. Requires See Source Code permission on file's project<br/>" +
         "Each element of the result array is an object which contains:" +
         "<ol>" +
         "<li>Line number</li>" +
@@ -153,7 +150,7 @@ public class LinesAction implements SourcesWsAction {
       Iterable<DbFileSources.Line> lines = checkFoundWithOptional(sourceService.getLines(dbSession, file.uuid(), from, to), "No source found for file '%s'", file.getDbKey());
       try (JsonWriter json = response.newJsonWriter()) {
         json.beginObject();
-        writeSource(lines, json, isMemberOfOrganization(dbSession, file), periodDateSupplier);
+        linesJsonWriter.writeSource(lines, json, isMemberOfOrganization(dbSession, file), periodDateSupplier);
         json.endObject();
       }
     }
@@ -179,88 +176,4 @@ public class LinesAction implements SourcesWsAction {
     checkRequest(componentKey != null, "The '%s' parameter is missing", PARAM_KEY);
     return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, branch, pullRequest);
   }
-
-  private void writeSource(Iterable<DbFileSources.Line> lines, JsonWriter json, boolean filterScmAuthors, Supplier<Optional<Long>> periodDateSupplier) {
-    Optional<Long> periodDate = null;
-
-    json.name("sources").beginArray();
-    for (DbFileSources.Line line : lines) {
-      json.beginObject()
-        .prop("line", line.getLine())
-        .prop("code", htmlSourceDecorator.getDecoratedSourceAsHtml(line.getSource(), line.getHighlighting(), line.getSymbols()))
-        .prop("scmRevision", line.getScmRevision());
-      if (!filterScmAuthors) {
-        json.prop("scmAuthor", line.getScmAuthor());
-      }
-      if (line.hasScmDate()) {
-        json.prop("scmDate", DateUtils.formatDateTime(new Date(line.getScmDate())));
-      }
-      Optional<Integer> lineHits = getLineHits(line);
-      if (lineHits.isPresent()) {
-        json.prop("utLineHits", lineHits.get());
-        json.prop("lineHits", lineHits.get());
-      }
-      Optional<Integer> conditions = getConditions(line);
-      if (conditions.isPresent()) {
-        json.prop("utConditions", conditions.get());
-        json.prop("conditions", conditions.get());
-      }
-      Optional<Integer> coveredConditions = getCoveredConditions(line);
-      if (coveredConditions.isPresent()) {
-        json.prop("utCoveredConditions", coveredConditions.get());
-        json.prop("coveredConditions", coveredConditions.get());
-      }
-      json.prop("duplicated", line.getDuplicationCount() > 0);
-      if (line.hasIsNewLine()) {
-        json.prop("isNew", line.getIsNewLine());
-      } else {
-        if (periodDate == null) {
-          periodDate = periodDateSupplier.get();
-        }
-        json.prop("isNew", periodDate.isPresent() && line.getScmDate() > periodDate.get());
-      }
-      json.endObject();
-    }
-    json.endArray();
-  }
-
-  private static Optional<Integer> getLineHits(DbFileSources.Line line) {
-    if (line.hasLineHits()) {
-      return Optional.of(line.getLineHits());
-    } else if (line.hasDeprecatedOverallLineHits()) {
-      return Optional.of(line.getDeprecatedOverallLineHits());
-    } else if (line.hasDeprecatedUtLineHits()) {
-      return Optional.of(line.getDeprecatedUtLineHits());
-    } else if (line.hasDeprecatedItLineHits()) {
-      return Optional.of(line.getDeprecatedItLineHits());
-    }
-    return Optional.empty();
-  }
-
-  private static Optional<Integer> getConditions(DbFileSources.Line line) {
-    if (line.hasConditions()) {
-      return Optional.of(line.getConditions());
-    } else if (line.hasDeprecatedOverallConditions()) {
-      return Optional.of(line.getDeprecatedOverallConditions());
-    } else if (line.hasDeprecatedUtConditions()) {
-      return Optional.of(line.getDeprecatedUtConditions());
-    } else if (line.hasDeprecatedItConditions()) {
-      return Optional.of(line.getDeprecatedItConditions());
-    }
-    return Optional.empty();
-  }
-
-  private static Optional<Integer> getCoveredConditions(DbFileSources.Line line) {
-    if (line.hasCoveredConditions()) {
-      return Optional.of(line.getCoveredConditions());
-    } else if (line.hasDeprecatedOverallCoveredConditions()) {
-      return Optional.of(line.getDeprecatedOverallCoveredConditions());
-    } else if (line.hasDeprecatedUtCoveredConditions()) {
-      return Optional.of(line.getDeprecatedUtCoveredConditions());
-    } else if (line.hasDeprecatedItCoveredConditions()) {
-      return Optional.of(line.getDeprecatedItCoveredConditions());
-    }
-    return Optional.empty();
-  }
-
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesJsonWriter.java b/server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesJsonWriter.java
new file mode 100644 (file)
index 0000000..d85ac5d
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * 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.source.ws;
+
+import java.util.Date;
+import java.util.Optional;
+import java.util.function.Supplier;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.db.protobuf.DbFileSources;
+import org.sonar.server.source.HtmlSourceDecorator;
+
+public class LinesJsonWriter {
+  private final HtmlSourceDecorator htmlSourceDecorator;
+
+  public LinesJsonWriter(HtmlSourceDecorator htmlSourceDecorator) {
+    this.htmlSourceDecorator = htmlSourceDecorator;
+  }
+
+  public void writeSource(Iterable<DbFileSources.Line> lines, JsonWriter json, boolean filterScmAuthors, Supplier<Optional<Long>> periodDateSupplier) {
+    Optional<Long> periodDate = null;
+
+    json.name("sources").beginArray();
+    for (DbFileSources.Line line : lines) {
+      json.beginObject()
+        .prop("line", line.getLine())
+        .prop("code", htmlSourceDecorator.getDecoratedSourceAsHtml(line.getSource(), line.getHighlighting(), line.getSymbols()))
+        .prop("scmRevision", line.getScmRevision());
+      if (!filterScmAuthors) {
+        json.prop("scmAuthor", line.getScmAuthor());
+      }
+      if (line.hasScmDate()) {
+        json.prop("scmDate", DateUtils.formatDateTime(new Date(line.getScmDate())));
+      }
+      Optional<Integer> lineHits = getLineHits(line);
+      if (lineHits.isPresent()) {
+        json.prop("utLineHits", lineHits.get());
+        json.prop("lineHits", lineHits.get());
+      }
+      Optional<Integer> conditions = getConditions(line);
+      if (conditions.isPresent()) {
+        json.prop("utConditions", conditions.get());
+        json.prop("conditions", conditions.get());
+      }
+      Optional<Integer> coveredConditions = getCoveredConditions(line);
+      if (coveredConditions.isPresent()) {
+        json.prop("utCoveredConditions", coveredConditions.get());
+        json.prop("coveredConditions", coveredConditions.get());
+      }
+      json.prop("duplicated", line.getDuplicationCount() > 0);
+      if (line.hasIsNewLine()) {
+        json.prop("isNew", line.getIsNewLine());
+      } else {
+        if (periodDate == null) {
+          periodDate = periodDateSupplier.get();
+        }
+        json.prop("isNew", periodDate.isPresent() && line.getScmDate() > periodDate.get());
+      }
+      json.endObject();
+    }
+    json.endArray();
+  }
+
+  private static Optional<Integer> getLineHits(DbFileSources.Line line) {
+    if (line.hasLineHits()) {
+      return Optional.of(line.getLineHits());
+    } else if (line.hasDeprecatedOverallLineHits()) {
+      return Optional.of(line.getDeprecatedOverallLineHits());
+    } else if (line.hasDeprecatedUtLineHits()) {
+      return Optional.of(line.getDeprecatedUtLineHits());
+    } else if (line.hasDeprecatedItLineHits()) {
+      return Optional.of(line.getDeprecatedItLineHits());
+    }
+    return Optional.empty();
+  }
+
+  private static Optional<Integer> getConditions(DbFileSources.Line line) {
+    if (line.hasConditions()) {
+      return Optional.of(line.getConditions());
+    } else if (line.hasDeprecatedOverallConditions()) {
+      return Optional.of(line.getDeprecatedOverallConditions());
+    } else if (line.hasDeprecatedUtConditions()) {
+      return Optional.of(line.getDeprecatedUtConditions());
+    } else if (line.hasDeprecatedItConditions()) {
+      return Optional.of(line.getDeprecatedItConditions());
+    }
+    return Optional.empty();
+  }
+
+  private static Optional<Integer> getCoveredConditions(DbFileSources.Line line) {
+    if (line.hasCoveredConditions()) {
+      return Optional.of(line.getCoveredConditions());
+    } else if (line.hasDeprecatedOverallCoveredConditions()) {
+      return Optional.of(line.getDeprecatedOverallCoveredConditions());
+    } else if (line.hasDeprecatedUtCoveredConditions()) {
+      return Optional.of(line.getDeprecatedUtCoveredConditions());
+    } else if (line.hasDeprecatedItCoveredConditions()) {
+      return Optional.of(line.getDeprecatedItCoveredConditions());
+    }
+    return Optional.empty();
+  }
+}
index d41b10c1ce72b236bac2af0cd7d65af6661c655d..6d9208856f8b993e4999671ee6ec3a6076f76722 100644 (file)
@@ -52,7 +52,7 @@ public class ShowAction implements SourcesWsAction {
   @Override
   public void define(WebService.NewController controller) {
     WebService.NewAction action = controller.createAction("show")
-      .setDescription("Get source code. Require See Source Code permission on file's project<br/>" +
+      .setDescription("Get source code. Requires See Source Code permission on file's project<br/>" +
         "Each element of the result array is composed of:" +
         "<ol>" +
         "<li>Line number</li>" +
index 8a7b6354999e70f1ff5bbc3923184c8bd00c66ba..647eca99f4f4f3aad4274a58220e025ecd4d2ace 100644 (file)
@@ -55,7 +55,10 @@ public class AppActionTest {
   @Rule
   public DbTester db = DbTester.create();
 
-  private WsActionTester ws = new WsActionTester(new AppAction(db.getDbClient(), userSession, TestComponentFinder.from(db)));
+  private ComponentViewerJsonWriter componentViewerJsonWriter = new ComponentViewerJsonWriter(db.getDbClient());
+
+  private WsActionTester ws = new WsActionTester(new AppAction(db.getDbClient(), userSession,
+    TestComponentFinder.from(db), componentViewerJsonWriter));
 
   @Test
   public void file_info() {
index ccf0ddc2496cc83b6958f5a2302c1b73cd5e8300..58d1c0b1c7921dfc74d35746667cd16a4ebcc94d 100644 (file)
 package org.sonar.server.source;
 
 import com.google.common.collect.Lists;
-import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -52,7 +54,7 @@ public class SourceServiceTest {
   SourceService underTest = new SourceService(dbTester.getDbClient(), htmlDecorator);
 
   @Before
-  public void injectFakeLines() throws IOException {
+  public void injectFakeLines() {
     FileSourceDto dto = new FileSourceDto();
     dto.setFileUuid(FILE_UUID).setProjectUuid("PROJECT_UUID");
     dto.setSourceData(FileSourceTesting.newFakeData(10).build());
@@ -62,6 +64,18 @@ public class SourceServiceTest {
 
   @Test
   public void get_range_of_lines() {
+    Set<Integer> lineNumbers = new HashSet<>(Arrays.asList(1, 5, 6));
+    Optional<Iterable<DbFileSources.Line>> linesOpt = underTest.getLines(dbTester.getSession(), FILE_UUID, lineNumbers);
+    assertThat(linesOpt.isPresent()).isTrue();
+    List<DbFileSources.Line> lines = Lists.newArrayList(linesOpt.get());
+    assertThat(lines).hasSize(3);
+    assertThat(lines.get(0).getLine()).isEqualTo(1);
+    assertThat(lines.get(1).getLine()).isEqualTo(5);
+    assertThat(lines.get(2).getLine()).isEqualTo(6);
+  }
+
+  @Test
+  public void get_set_of_lines() {
     Optional<Iterable<DbFileSources.Line>> linesOpt = underTest.getLines(dbTester.getSession(), FILE_UUID, 5, 7);
     assertThat(linesOpt.isPresent()).isTrue();
     List<DbFileSources.Line> lines = Lists.newArrayList(linesOpt.get());
diff --git a/server/sonar-server/src/test/java/org/sonar/server/source/ws/IssueSnippetsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/source/ws/IssueSnippetsActionTest.java
new file mode 100644 (file)
index 0000000..2dd813d
--- /dev/null
@@ -0,0 +1,246 @@
+/*
+ * 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.source.ws;
+
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.stubbing.Answer;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.protobuf.DbCommons;
+import org.sonar.db.protobuf.DbFileSources;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.source.FileSourceTester;
+import org.sonar.server.component.ws.ComponentViewerJsonWriter;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.issue.IssueFinder;
+import org.sonar.server.source.HtmlSourceDecorator;
+import org.sonar.server.source.SourceService;
+import org.sonar.server.source.index.FileSourceTesting;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestResponse;
+import org.sonar.server.ws.WsActionTester;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY;
+import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY_KEY;
+import static org.sonar.api.measures.CoreMetrics.LINES_KEY;
+import static org.sonar.api.measures.CoreMetrics.TECHNICAL_DEBT_KEY;
+import static org.sonar.api.measures.CoreMetrics.TESTS_KEY;
+import static org.sonar.api.measures.CoreMetrics.VIOLATIONS_KEY;
+import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+
+public class IssueSnippetsActionTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  @Rule
+  public DbTester db = DbTester.create(System2.INSTANCE);
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+
+  private DbClient dbClient = db.getDbClient();
+  private FileSourceTester fileSourceTester = new FileSourceTester(db);
+  private ComponentDto project;
+  private WsActionTester actionTester;
+
+  @Before
+  public void setUp() {
+    OrganizationDto organization = db.organizations().insert();
+    project = db.components().insertPrivateProject(organization, "projectUuid");
+
+    HtmlSourceDecorator htmlSourceDecorator = mock(HtmlSourceDecorator.class);
+    when(htmlSourceDecorator.getDecoratedSourceAsHtml(anyString(), anyString(), anyString())).then((Answer<String>)
+      invocationOnMock -> "<p>" + invocationOnMock.getArguments()[0] + "</p>");
+    LinesJsonWriter linesJsonWriter = new LinesJsonWriter(htmlSourceDecorator);
+    IssueFinder issueFinder = new IssueFinder(dbClient, userSession);
+    ComponentViewerJsonWriter componentViewerJsonWriter = new ComponentViewerJsonWriter(dbClient);
+    SourceService sourceService = new SourceService(dbClient, htmlSourceDecorator);
+    actionTester = new WsActionTester(new IssueSnippetsAction(sourceService, dbClient, issueFinder, linesJsonWriter, componentViewerJsonWriter));
+  }
+
+  @Test
+  public void should_display_single_location_single_file() {
+    ComponentDto file = insertFile(project, "file");
+    DbFileSources.Data fileSources = FileSourceTesting.newFakeData(10).build();
+    fileSourceTester.insertFileSource(file, 10, dto -> dto.setSourceData(fileSources));
+    userSession.logIn().addProjectPermission(USER, project, file);
+
+    String issueKey = insertIssue(file, newLocation(file.uuid(), 5, 5));
+
+    TestResponse response = actionTester.newRequest().setParam("issueKey", issueKey).execute();
+    response.assertJson(getClass(), "issue_snippets_single_location.json");
+  }
+
+  @Test
+  public void should_add_measures_to_components() {
+    ComponentDto file = insertFile(project, "file");
+
+    MetricDto lines = db.measures().insertMetric(m -> m.setKey(LINES_KEY));
+    db.measures().insertLiveMeasure(file, lines, m -> m.setValue(200d));
+    MetricDto duplicatedLines = db.measures().insertMetric(m -> m.setKey(DUPLICATED_LINES_DENSITY_KEY));
+    db.measures().insertLiveMeasure(file, duplicatedLines, m -> m.setValue(7.4));
+    MetricDto tests = db.measures().insertMetric(m -> m.setKey(TESTS_KEY));
+    db.measures().insertLiveMeasure(file, tests, m -> m.setValue(3d));
+    MetricDto technicalDebt = db.measures().insertMetric(m -> m.setKey(TECHNICAL_DEBT_KEY));
+    db.measures().insertLiveMeasure(file, technicalDebt, m -> m.setValue(182d));
+    MetricDto issues = db.measures().insertMetric(m -> m.setKey(VIOLATIONS_KEY));
+    db.measures().insertLiveMeasure(file, issues, m -> m.setValue(231d));
+    MetricDto coverage = db.measures().insertMetric(m -> m.setKey(COVERAGE_KEY));
+    db.measures().insertLiveMeasure(file, coverage, m -> m.setValue(95.4d));
+
+    DbFileSources.Data fileSources = FileSourceTesting.newFakeData(10).build();
+    fileSourceTester.insertFileSource(file, 10, dto -> dto.setSourceData(fileSources));
+    userSession.logIn().addProjectPermission(USER, project, file);
+
+    String issueKey = insertIssue(file, newLocation(file.uuid(), 5, 5));
+
+    TestResponse response = actionTester.newRequest().setParam("issueKey", issueKey).execute();
+    response.assertJson(getClass(), "issue_snippets_with_measures.json");
+  }
+
+  @Test
+  public void issue_references_a_non_existing_component() {
+    ComponentDto file = insertFile(project, "file");
+    ComponentDto file2 = newFileDto(project, null, "nonexisting");
+
+    DbFileSources.Data fileSources = FileSourceTesting.newFakeData(10).build();
+    fileSourceTester.insertFileSource(file, 10, dto -> dto.setSourceData(fileSources));
+    userSession.logIn().addProjectPermission(USER, project, file);
+
+    String issueKey = insertIssue(file, newLocation(file2.uuid(), 5, 5));
+
+    TestResponse response = actionTester.newRequest().setParam("issueKey", issueKey).execute();
+    response.assertJson("{}");
+  }
+
+  @Test
+  public void no_code_to_display() {
+    ComponentDto file = insertFile(project, "file");
+    userSession.logIn().addProjectPermission(USER, project, file);
+
+    String issueKey = insertIssue(file, newLocation(file.uuid(), 5, 5));
+
+    TestResponse response = actionTester.newRequest().setParam("issueKey", issueKey).execute();
+    response.assertJson("{}");
+  }
+
+  @Test
+  public void fail_if_no_project_permission() {
+    ComponentDto file = insertFile(project, "file");
+    String issueKey = insertIssue(file, newLocation(file.uuid(), 5, 5));
+
+    expectedException.expect(ForbiddenException.class);
+    actionTester.newRequest().setParam("issueKey", issueKey).execute();
+  }
+
+  @Test
+  public void fail_if_issue_not_found() {
+    ComponentDto file = insertFile(project, "file");
+    insertIssue(file, newLocation(file.uuid(), 5, 5));
+    userSession.logIn().addProjectPermission(USER, project, file);
+
+    expectedException.expect(NotFoundException.class);
+    expectedException.expectMessage("Issue with key 'invalid' does not exist");
+    actionTester.newRequest().setParam("issueKey", "invalid").execute();
+  }
+
+  @Test
+  public void fail_if_parameter_missing() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("The 'issueKey' parameter is missing");
+    actionTester.newRequest().execute();
+  }
+
+  @Test
+  public void should_display_multiple_locations_multiple_files() {
+    ComponentDto file1 = insertFile(project, "file1");
+    ComponentDto file2 = insertFile(project, "file2");
+
+    DbFileSources.Data fileSources = FileSourceTesting.newFakeData(10).build();
+    fileSourceTester.insertFileSource(file1, 10, dto -> dto.setSourceData(fileSources));
+    fileSourceTester.insertFileSource(file2, 10, dto -> dto.setSourceData(fileSources));
+
+    userSession.logIn().addProjectPermission(USER, project, file1, file2);
+
+    String issueKey1 = insertIssue(file1, newLocation(file1.uuid(), 5, 5),
+      newLocation(file1.uuid(), 9, 9), newLocation(file2.uuid(), 1, 5));
+
+    TestResponse response = actionTester.newRequest().setParam("issueKey", issueKey1).execute();
+    response.assertJson(getClass(), "issue_snippets_multiple_locations.json");
+  }
+
+  @Test
+  public void should_connect_snippets_close_to_each_other() {
+    ComponentDto file1 = insertFile(project, "file1");
+
+    DbFileSources.Data fileSources = FileSourceTesting.newFakeData(20).build();
+    fileSourceTester.insertFileSource(file1, 20, dto -> dto.setSourceData(fileSources));
+
+    userSession.logIn().addProjectPermission(USER, project, file1);
+
+    // these two locations should get connected, making a single range 3-14
+    String issueKey1 = insertIssue(file1, newLocation(file1.uuid(), 5, 5),
+      newLocation(file1.uuid(), 12, 12));
+
+    TestResponse response = actionTester.newRequest().setParam("issueKey", issueKey1).execute();
+    response.assertJson(getClass(), "issue_snippets_close_to_each_other.json");
+  }
+
+  private DbIssues.Location newLocation(String fileUuid, int startLine, int endLine) {
+    return DbIssues.Location.newBuilder()
+      .setTextRange(DbCommons.TextRange.newBuilder().setStartLine(startLine).setEndLine(endLine).build())
+      .setComponentId(fileUuid)
+      .build();
+  }
+
+  private ComponentDto insertFile(ComponentDto project, String name) {
+    ComponentDto file = newFileDto(project, null, name);
+    db.components().insertComponents(file);
+    return file;
+  }
+
+  private String insertIssue(ComponentDto file, DbIssues.Location... locations) {
+    RuleDefinitionDto rule = db.rules().insert();
+    DbIssues.Flow flow = DbIssues.Flow.newBuilder().addAllLocation(Arrays.asList(locations)).build();
+
+    IssueDto issue = db.issues().insert(rule, project, file, i -> {
+      i.setLocations(DbIssues.Locations.newBuilder().addFlow(flow).build());
+      i.setProjectUuid(project.uuid());
+      i.setLine(locations[0].getTextRange().getStartLine());
+    });
+
+    db.commit();
+    return issue.getKey();
+  }
+
+}
index 060f1042007485ee274c107c4a0bc4841679d8f4..28808af5d90a0161f3a28eeea86c4c022394c71b 100644 (file)
@@ -73,9 +73,10 @@ public class LinesActionTest {
     HtmlSourceDecorator htmlSourceDecorator = mock(HtmlSourceDecorator.class);
     when(htmlSourceDecorator.getDecoratedSourceAsHtml(anyString(), anyString(), anyString())).then((Answer<String>)
       invocationOnMock -> "<p>" + invocationOnMock.getArguments()[0] + "</p>");
+    LinesJsonWriter linesJsonWriter = new LinesJsonWriter(htmlSourceDecorator);
     SourceService sourceService = new SourceService(db.getDbClient(), htmlSourceDecorator);
     wsTester = new WsTester(new SourcesWs(
-      new LinesAction(TestComponentFinder.from(db), db.getDbClient(), sourceService, htmlSourceDecorator, userSession)));
+      new LinesAction(TestComponentFinder.from(db), db.getDbClient(), sourceService, linesJsonWriter, userSession)));
     organization = db.organizations().insert();
     privateProject = ComponentTesting.newPrivateProjectDto(organization);
   }
index 94802cb670d6e1495f9a0aa39191288ca9f6b962..c0c6cefc2ed56cad90a41a53d6b97c5ac9ab8098 100644 (file)
@@ -24,7 +24,8 @@ import org.junit.Test;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.db.DbClient;
 import org.sonar.server.component.ComponentFinder;
-import org.sonar.server.source.HtmlSourceDecorator;
+import org.sonar.server.component.ws.ComponentViewerJsonWriter;
+import org.sonar.server.issue.IssueFinder;
 import org.sonar.server.source.SourceService;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.WsTester;
@@ -38,9 +39,11 @@ public class SourcesWsTest {
 
   ShowAction showAction = new ShowAction(mock(SourceService.class), mock(DbClient.class), userSessionRule, mock(ComponentFinder.class));
   RawAction rawAction = new RawAction(mock(DbClient.class), mock(SourceService.class), userSessionRule, mock(ComponentFinder.class));
-  LinesAction linesAction = new LinesAction(mock(ComponentFinder.class), mock(DbClient.class), mock(SourceService.class), mock(HtmlSourceDecorator.class), userSessionRule);
+  LinesAction linesAction = new LinesAction(mock(ComponentFinder.class), mock(DbClient.class), mock(SourceService.class), mock(LinesJsonWriter.class), userSessionRule);
   HashAction hashAction = new HashAction(mock(DbClient.class), userSessionRule, mock(ComponentFinder.class));
-  WsTester tester = new WsTester(new SourcesWs(showAction, rawAction, linesAction, hashAction));
+  IssueSnippetsAction issueSnippetsAction = new IssueSnippetsAction(mock(SourceService.class), mock(DbClient.class), userSessionRule, mock(IssueFinder.class),
+    mock(LinesJsonWriter.class), mock(ComponentViewerJsonWriter.class));
+  WsTester tester = new WsTester(new SourcesWs(showAction, rawAction, linesAction, hashAction, issueSnippetsAction));
 
   @Test
   public void define_ws() {
@@ -48,7 +51,7 @@ public class SourcesWsTest {
     assertThat(controller).isNotNull();
     assertThat(controller.since()).isEqualTo("4.2");
     assertThat(controller.description()).isNotEmpty();
-    assertThat(controller.actions()).hasSize(4);
+    assertThat(controller.actions()).hasSize(5);
 
     WebService.Action show = controller.action("show");
     assertThat(show).isNotNull();
@@ -81,5 +84,13 @@ public class SourcesWsTest {
     assertThat(hash.isInternal()).isTrue();
     assertThat(hash.responseExampleAsString()).isNotEmpty();
     assertThat(hash.params()).hasSize(1);
+
+    WebService.Action issueSnippets = controller.action("issue_snippets");
+    assertThat(issueSnippets).isNotNull();
+    assertThat(issueSnippets.handler()).isSameAs(issueSnippetsAction);
+    assertThat(issueSnippets.since()).isEqualTo("7.8");
+    assertThat(issueSnippets.isInternal()).isTrue();
+    assertThat(issueSnippets.responseExampleAsString()).isNotEmpty();
+    assertThat(issueSnippets.params()).hasSize(1);
   }
 }
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_close_to_each_other.json b/server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_close_to_each_other.json
new file mode 100644 (file)
index 0000000..c728ef0
--- /dev/null
@@ -0,0 +1,185 @@
+{
+  "FILE_KEY_file1": {
+    "component": {
+      "key": "FILE_KEY_file1",
+      "uuid": "file1",
+      "path": "null/NAME_file1",
+      "name": "NAME_file1",
+      "longName": "null/NAME_file1",
+      "q": "FIL",
+      "project": "KEY_projectUuid",
+      "projectName": "LONG_NAME_projectUuid",
+      "measures": {}
+    },
+    "sources": [
+      {
+        "line": 3,
+        "code": "\u003cp\u003eSOURCE_3\u003c/p\u003e",
+        "scmRevision": "REVISION_3",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 3,
+        "lineHits": 3,
+        "utConditions": 4,
+        "conditions": 4,
+        "utCoveredConditions": 5,
+        "coveredConditions": 5,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 4,
+        "code": "\u003cp\u003eSOURCE_4\u003c/p\u003e",
+        "scmRevision": "REVISION_4",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 4,
+        "lineHits": 4,
+        "utConditions": 5,
+        "conditions": 5,
+        "utCoveredConditions": 6,
+        "coveredConditions": 6,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 5,
+        "code": "\u003cp\u003eSOURCE_5\u003c/p\u003e",
+        "scmRevision": "REVISION_5",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 5,
+        "lineHits": 5,
+        "utConditions": 6,
+        "conditions": 6,
+        "utCoveredConditions": 7,
+        "coveredConditions": 7,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 6,
+        "code": "\u003cp\u003eSOURCE_6\u003c/p\u003e",
+        "scmRevision": "REVISION_6",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 6,
+        "lineHits": 6,
+        "utConditions": 7,
+        "conditions": 7,
+        "utCoveredConditions": 8,
+        "coveredConditions": 8,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 7,
+        "code": "\u003cp\u003eSOURCE_7\u003c/p\u003e",
+        "scmRevision": "REVISION_7",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 7,
+        "lineHits": 7,
+        "utConditions": 8,
+        "conditions": 8,
+        "utCoveredConditions": 9,
+        "coveredConditions": 9,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 8,
+        "code": "\u003cp\u003eSOURCE_8\u003c/p\u003e",
+        "scmRevision": "REVISION_8",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 8,
+        "lineHits": 8,
+        "utConditions": 9,
+        "conditions": 9,
+        "utCoveredConditions": 10,
+        "coveredConditions": 10,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 9,
+        "code": "\u003cp\u003eSOURCE_9\u003c/p\u003e",
+        "scmRevision": "REVISION_9",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 9,
+        "lineHits": 9,
+        "utConditions": 10,
+        "conditions": 10,
+        "utCoveredConditions": 11,
+        "coveredConditions": 11,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 10,
+        "code": "\u003cp\u003eSOURCE_10\u003c/p\u003e",
+        "scmRevision": "REVISION_10",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 10,
+        "lineHits": 10,
+        "utConditions": 11,
+        "conditions": 11,
+        "utCoveredConditions": 12,
+        "coveredConditions": 12,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 11,
+        "code": "\u003cp\u003eSOURCE_11\u003c/p\u003e",
+        "scmRevision": "REVISION_11",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 11,
+        "lineHits": 11,
+        "utConditions": 12,
+        "conditions": 12,
+        "utCoveredConditions": 13,
+        "coveredConditions": 13,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 12,
+        "code": "\u003cp\u003eSOURCE_12\u003c/p\u003e",
+        "scmRevision": "REVISION_12",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 12,
+        "lineHits": 12,
+        "utConditions": 13,
+        "conditions": 13,
+        "utCoveredConditions": 14,
+        "coveredConditions": 14,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 13,
+        "code": "\u003cp\u003eSOURCE_13\u003c/p\u003e",
+        "scmRevision": "REVISION_13",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 13,
+        "lineHits": 13,
+        "utConditions": 14,
+        "conditions": 14,
+        "utCoveredConditions": 15,
+        "coveredConditions": 15,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 14,
+        "code": "\u003cp\u003eSOURCE_14\u003c/p\u003e",
+        "scmRevision": "REVISION_14",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 14,
+        "lineHits": 14,
+        "utConditions": 15,
+        "conditions": 15,
+        "utCoveredConditions": 16,
+        "coveredConditions": 16,
+        "duplicated": true,
+        "isNew": true
+      }
+    ]
+  }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_multiple_locations.json b/server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_multiple_locations.json
new file mode 100644 (file)
index 0000000..f4fe2d2
--- /dev/null
@@ -0,0 +1,240 @@
+{
+  "FILE_KEY_file2": {
+    "component": {
+      "key": "FILE_KEY_file2",
+      "uuid": "file2",
+      "path": "null/NAME_file2",
+      "name": "NAME_file2",
+      "longName": "null/NAME_file2",
+      "q": "FIL",
+      "project": "KEY_projectUuid",
+      "projectName": "LONG_NAME_projectUuid"
+    },
+    "sources": [
+      {
+        "line": 1,
+        "code": "<p>SOURCE_1</p>",
+        "scmRevision": "REVISION_1",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 1,
+        "lineHits": 1,
+        "utConditions": 2,
+        "conditions": 2,
+        "utCoveredConditions": 3,
+        "coveredConditions": 3,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 2,
+        "code": "<p>SOURCE_2</p>",
+        "scmRevision": "REVISION_2",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 2,
+        "lineHits": 2,
+        "utConditions": 3,
+        "conditions": 3,
+        "utCoveredConditions": 4,
+        "coveredConditions": 4,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 3,
+        "code": "<p>SOURCE_3</p>",
+        "scmRevision": "REVISION_3",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 3,
+        "lineHits": 3,
+        "utConditions": 4,
+        "conditions": 4,
+        "utCoveredConditions": 5,
+        "coveredConditions": 5,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 4,
+        "code": "<p>SOURCE_4</p>",
+        "scmRevision": "REVISION_4",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 4,
+        "lineHits": 4,
+        "utConditions": 5,
+        "conditions": 5,
+        "utCoveredConditions": 6,
+        "coveredConditions": 6,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 5,
+        "code": "<p>SOURCE_5</p>",
+        "scmRevision": "REVISION_5",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 5,
+        "lineHits": 5,
+        "utConditions": 6,
+        "conditions": 6,
+        "utCoveredConditions": 7,
+        "coveredConditions": 7,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 6,
+        "code": "<p>SOURCE_6</p>",
+        "scmRevision": "REVISION_6",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 6,
+        "lineHits": 6,
+        "utConditions": 7,
+        "conditions": 7,
+        "utCoveredConditions": 8,
+        "coveredConditions": 8,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 7,
+        "code": "<p>SOURCE_7</p>",
+        "scmRevision": "REVISION_7",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 7,
+        "lineHits": 7,
+        "utConditions": 8,
+        "conditions": 8,
+        "utCoveredConditions": 9,
+        "coveredConditions": 9,
+        "duplicated": true,
+        "isNew": true
+      }
+    ]
+  },
+  "FILE_KEY_file1": {
+    "component": {
+      "key": "FILE_KEY_file1",
+      "uuid": "file1",
+      "path": "null/NAME_file1",
+      "name": "NAME_file1",
+      "longName": "null/NAME_file1",
+      "q": "FIL",
+      "project": "KEY_projectUuid",
+      "projectName": "LONG_NAME_projectUuid"
+    },
+    "sources": [
+      {
+        "line": 3,
+        "code": "<p>SOURCE_3</p>",
+        "scmRevision": "REVISION_3",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 3,
+        "lineHits": 3,
+        "utConditions": 4,
+        "conditions": 4,
+        "utCoveredConditions": 5,
+        "coveredConditions": 5,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 4,
+        "code": "<p>SOURCE_4</p>",
+        "scmRevision": "REVISION_4",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 4,
+        "lineHits": 4,
+        "utConditions": 5,
+        "conditions": 5,
+        "utCoveredConditions": 6,
+        "coveredConditions": 6,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 5,
+        "code": "<p>SOURCE_5</p>",
+        "scmRevision": "REVISION_5",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 5,
+        "lineHits": 5,
+        "utConditions": 6,
+        "conditions": 6,
+        "utCoveredConditions": 7,
+        "coveredConditions": 7,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 6,
+        "code": "<p>SOURCE_6</p>",
+        "scmRevision": "REVISION_6",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 6,
+        "lineHits": 6,
+        "utConditions": 7,
+        "conditions": 7,
+        "utCoveredConditions": 8,
+        "coveredConditions": 8,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 7,
+        "code": "<p>SOURCE_7</p>",
+        "scmRevision": "REVISION_7",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 7,
+        "lineHits": 7,
+        "utConditions": 8,
+        "conditions": 8,
+        "utCoveredConditions": 9,
+        "coveredConditions": 9,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 8,
+        "code": "<p>SOURCE_8</p>",
+        "scmRevision": "REVISION_8",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 8,
+        "lineHits": 8,
+        "utConditions": 9,
+        "conditions": 9,
+        "utCoveredConditions": 10,
+        "coveredConditions": 10,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 9,
+        "code": "<p>SOURCE_9</p>",
+        "scmRevision": "REVISION_9",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 9,
+        "lineHits": 9,
+        "utConditions": 10,
+        "conditions": 10,
+        "utCoveredConditions": 11,
+        "coveredConditions": 11,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 10,
+        "code": "<p>SOURCE_10</p>",
+        "scmRevision": "REVISION_10",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 10,
+        "lineHits": 10,
+        "utConditions": 11,
+        "conditions": 11,
+        "utCoveredConditions": 12,
+        "coveredConditions": 12,
+        "duplicated": true,
+        "isNew": true
+      }
+    ]
+  }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_single_location.json b/server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_single_location.json
new file mode 100644 (file)
index 0000000..9935b29
--- /dev/null
@@ -0,0 +1,86 @@
+{
+  "FILE_KEY_file": {
+    "component": {
+      "key": "FILE_KEY_file",
+      "uuid": "file",
+      "path": "null/NAME_file",
+      "name": "NAME_file",
+      "longName": "null/NAME_file",
+      "q": "FIL",
+      "project": "KEY_projectUuid",
+      "projectName": "LONG_NAME_projectUuid"
+    },
+    "sources": [
+      {
+        "line": 3,
+        "code": "\u003cp\u003eSOURCE_3\u003c/p\u003e",
+        "scmRevision": "REVISION_3",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 3,
+        "lineHits": 3,
+        "utConditions": 4,
+        "conditions": 4,
+        "utCoveredConditions": 5,
+        "coveredConditions": 5,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 4,
+        "code": "\u003cp\u003eSOURCE_4\u003c/p\u003e",
+        "scmRevision": "REVISION_4",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 4,
+        "lineHits": 4,
+        "utConditions": 5,
+        "conditions": 5,
+        "utCoveredConditions": 6,
+        "coveredConditions": 6,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 5,
+        "code": "\u003cp\u003eSOURCE_5\u003c/p\u003e",
+        "scmRevision": "REVISION_5",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 5,
+        "lineHits": 5,
+        "utConditions": 6,
+        "conditions": 6,
+        "utCoveredConditions": 7,
+        "coveredConditions": 7,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 6,
+        "code": "\u003cp\u003eSOURCE_6\u003c/p\u003e",
+        "scmRevision": "REVISION_6",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 6,
+        "lineHits": 6,
+        "utConditions": 7,
+        "conditions": 7,
+        "utCoveredConditions": 8,
+        "coveredConditions": 8,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 7,
+        "code": "\u003cp\u003eSOURCE_7\u003c/p\u003e",
+        "scmRevision": "REVISION_7",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 7,
+        "lineHits": 7,
+        "utConditions": 8,
+        "conditions": 8,
+        "utCoveredConditions": 9,
+        "coveredConditions": 9,
+        "duplicated": true,
+        "isNew": true
+      }
+    ]
+  }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_with_measures.json b/server/sonar-server/src/test/resources/org/sonar/server/source/ws/IssueSnippetsActionTest/issue_snippets_with_measures.json
new file mode 100644 (file)
index 0000000..9d1e9a9
--- /dev/null
@@ -0,0 +1,91 @@
+{
+  "FILE_KEY_file": {
+    "component": {
+      "key": "FILE_KEY_file",
+      "uuid": "file",
+      "path": "null/NAME_file",
+      "name": "NAME_file",
+      "longName": "null/NAME_file",
+      "q": "FIL",
+      "project": "KEY_projectUuid",
+      "projectName": "LONG_NAME_projectUuid",
+      "measures": {
+        "lines": "200.0",
+        "coverage": "95.4",
+        "duplicationDensity": "7.4"
+      }
+    },
+    "sources": [
+      {
+        "line": 3,
+        "code": "\u003cp\u003eSOURCE_3\u003c/p\u003e",
+        "scmRevision": "REVISION_3",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 3,
+        "lineHits": 3,
+        "utConditions": 4,
+        "conditions": 4,
+        "utCoveredConditions": 5,
+        "coveredConditions": 5,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 4,
+        "code": "\u003cp\u003eSOURCE_4\u003c/p\u003e",
+        "scmRevision": "REVISION_4",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 4,
+        "lineHits": 4,
+        "utConditions": 5,
+        "conditions": 5,
+        "utCoveredConditions": 6,
+        "coveredConditions": 6,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 5,
+        "code": "\u003cp\u003eSOURCE_5\u003c/p\u003e",
+        "scmRevision": "REVISION_5",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 5,
+        "lineHits": 5,
+        "utConditions": 6,
+        "conditions": 6,
+        "utCoveredConditions": 7,
+        "coveredConditions": 7,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 6,
+        "code": "\u003cp\u003eSOURCE_6\u003c/p\u003e",
+        "scmRevision": "REVISION_6",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 6,
+        "lineHits": 6,
+        "utConditions": 7,
+        "conditions": 7,
+        "utCoveredConditions": 8,
+        "coveredConditions": 8,
+        "duplicated": true,
+        "isNew": true
+      },
+      {
+        "line": 7,
+        "code": "\u003cp\u003eSOURCE_7\u003c/p\u003e",
+        "scmRevision": "REVISION_7",
+        "scmDate": "1974-10-02T21:40:00-0500",
+        "utLineHits": 7,
+        "lineHits": 7,
+        "utConditions": 8,
+        "conditions": 8,
+        "utCoveredConditions": 9,
+        "coveredConditions": 9,
+        "duplicated": true,
+        "isNew": true
+      }
+    ]
+  }
+}