]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9441 WS api/duplications/show fails properly when no parameter provided
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Tue, 20 Jun 2017 09:41:57 +0000 (11:41 +0200)
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Tue, 20 Jun 2017 13:16:02 +0000 (15:16 +0200)
13 files changed:
it/it-tests/src/test/java/it/duplication/DuplicationsTest.java
server/sonar-server/src/main/java/org/sonar/server/duplication/ws/DuplicationsJsonWriter.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/duplication/ws/DuplicationsWsAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/duplication/ws/ShowAction.java
server/sonar-server/src/main/java/org/sonar/server/duplication/ws/ShowResponseBuilder.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/resources/org/sonar/server/duplication/ws/example-show.json [deleted file]
server/sonar-server/src/main/resources/org/sonar/server/duplication/ws/show-example.json [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/duplication/ws/DuplicationsJsonWriterTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/duplication/ws/DuplicationsWsTest.java
server/sonar-server/src/test/java/org/sonar/server/duplication/ws/ShowActionTest.java
server/sonar-server/src/test/java/org/sonar/server/duplication/ws/ShowResponseBuilderTest.java [new file with mode: 0644]
sonar-ws/src/main/protobuf/ws-duplications.proto [new file with mode: 0644]

index ea55e0eb24b96c4571b94b53a5c3acd4add600e4..6eddcee5915595fe0b0aeb8c4c7da81003e4c724 100644 (file)
@@ -28,14 +28,19 @@ import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Test;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsResponse;
 import org.sonarqube.ws.client.issue.SearchWsRequest;
 import util.ItUtils;
 import util.issue.IssueRule;
 
+import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
 import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.skyscreamer.jsonassert.JSONAssert.assertEquals;
 import static util.ItUtils.getMeasuresAsDoubleByMetricKey;
+import static util.ItUtils.newAdminWsClient;
 import static util.ItUtils.runProjectAnalysis;
 import static util.ItUtils.setServerProperty;
 
@@ -51,6 +56,8 @@ public class DuplicationsTest {
   @ClassRule
   public static final IssueRule issueRule = IssueRule.from(orchestrator);
 
+  private static WsClient adminWsClient;
+
   @BeforeClass
   public static void analyzeProjects() {
     orchestrator.resetData();
@@ -62,6 +69,8 @@ public class DuplicationsTest {
     // Set minimum tokens to a big value in order to not get duplications
     setServerProperty(orchestrator, "sonar.cpd.xoo.minimumTokens", "1000");
     analyzeProject(WITHOUT_ENOUGH_TOKENS);
+
+    adminWsClient = newAdminWsClient(orchestrator);
   }
 
   @AfterClass
@@ -69,6 +78,37 @@ public class DuplicationsTest {
     setServerProperty(orchestrator, "sonar.cpd.xoo.minimumTokens", null);
   }
 
+  private static Map<String, Double> getMeasures(String key) {
+    return getMeasuresAsDoubleByMetricKey(orchestrator, key, "duplicated_lines", "duplicated_blocks", "duplicated_files", "duplicated_lines_density");
+  }
+
+  private static void verifyDuplicationMeasures(String componentKey, int duplicatedBlocks, int duplicatedLines, int duplicatedFiles, double duplicatedLinesDensity) {
+    Map<String, Double> measures = getMeasures(componentKey);
+    assertThat(measures.get("duplicated_blocks").intValue()).isEqualTo(duplicatedBlocks);
+    assertThat(measures.get("duplicated_lines").intValue()).isEqualTo(duplicatedLines);
+    assertThat(measures.get("duplicated_files").intValue()).isEqualTo(duplicatedFiles);
+    assertThat(measures.get("duplicated_lines_density")).isEqualTo(duplicatedLinesDensity);
+  }
+
+  private static void analyzeProject(String projectKey, String... additionalProperties) {
+    orchestrator.getServer().provisionProject(projectKey, projectKey);
+    orchestrator.getServer().associateProjectToQualityProfile(projectKey, "xoo", "xoo-duplication-profile");
+
+    runProjectAnalysis(orchestrator, "duplications/file-duplications",
+      ObjectArrays.concat(
+        new String[] {
+          "sonar.projectKey", projectKey,
+          "sonar.projectName", projectKey
+        },
+        additionalProperties, String.class));
+  }
+
+  private static void verifyWsResultOnDuplicateFile(String fileKey, String ws, String expectedFilePath) throws Exception {
+    String duplication = orchestrator.getServer().adminWsClient().get(ws, "key", fileKey);
+    assertEquals(IOUtils.toString(CrossProjectDuplicationsTest.class.getResourceAsStream("/duplication/DuplicationsTest/" + expectedFilePath), "UTF-8"), duplication,
+      false);
+  }
+
   @Test
   public void duplicated_lines_within_same_file() {
     verifyDuplicationMeasures(DUPLICATIONS + ":src/main/xoo/duplicated_lines_within_same_file/DuplicatedLinesInSameFile.xoo",
@@ -137,35 +177,13 @@ public class DuplicationsTest {
       "api/duplications/show", "duplications_show-expected.json");
   }
 
-  private static Map<String, Double> getMeasures(String key) {
-    return getMeasuresAsDoubleByMetricKey(orchestrator, key, "duplicated_lines", "duplicated_blocks", "duplicated_files", "duplicated_lines_density");
-  }
-
-  private static void verifyDuplicationMeasures(String componentKey, int duplicatedBlocks, int duplicatedLines, int duplicatedFiles, double duplicatedLinesDensity) {
-    Map<String, Double> measures = getMeasures(componentKey);
-    assertThat(measures.get("duplicated_blocks").intValue()).isEqualTo(duplicatedBlocks);
-    assertThat(measures.get("duplicated_lines").intValue()).isEqualTo(duplicatedLines);
-    assertThat(measures.get("duplicated_files").intValue()).isEqualTo(duplicatedFiles);
-    assertThat(measures.get("duplicated_lines_density")).isEqualTo(duplicatedLinesDensity);
-  }
-
-  private static void analyzeProject(String projectKey, String... additionalProperties) {
-    orchestrator.getServer().provisionProject(projectKey, projectKey);
-    orchestrator.getServer().associateProjectToQualityProfile(projectKey, "xoo", "xoo-duplication-profile");
-
-    runProjectAnalysis(orchestrator, "duplications/file-duplications",
-      ObjectArrays.concat(
-        new String[] {
-          "sonar.projectKey", projectKey,
-          "sonar.projectName", projectKey
-        },
-        additionalProperties, String.class));
-  }
+  // SONAR-9441
+  @Test
+  public void fail_properly_when_no_parameter() {
+    WsResponse result = adminWsClient.wsConnector().call(new GetRequest("api/duplications/show"));
 
-  private static void verifyWsResultOnDuplicateFile(String fileKey, String ws, String expectedFilePath) throws Exception {
-    String duplication = orchestrator.getServer().adminWsClient().get(ws, "key", fileKey);
-    assertEquals(IOUtils.toString(CrossProjectDuplicationsTest.class.getResourceAsStream("/duplication/DuplicationsTest/" + expectedFilePath), "UTF-8"), duplication,
-      false);
+    assertThat(result.code()).isEqualTo(HTTP_BAD_REQUEST);
+    assertThat(result.content()).contains("Either 'uuid' or 'key' must be provided, not both");
   }
 
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/duplication/ws/DuplicationsJsonWriter.java b/server/sonar-server/src/main/java/org/sonar/server/duplication/ws/DuplicationsJsonWriter.java
deleted file mode 100644 (file)
index 4dc6513..0000000
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.duplication.ws;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Optional;
-import java.util.List;
-import java.util.Map;
-import javax.annotation.Nullable;
-import org.sonar.api.server.ServerSide;
-import org.sonar.api.utils.text.JsonWriter;
-import org.sonar.db.DbSession;
-import org.sonar.db.component.ComponentDao;
-import org.sonar.db.component.ComponentDto;
-
-import static com.google.common.collect.Maps.newHashMap;
-
-@ServerSide
-public class DuplicationsJsonWriter {
-
-  private final ComponentDao componentDao;
-
-  public DuplicationsJsonWriter(ComponentDao componentDao) {
-    this.componentDao = componentDao;
-  }
-
-  @VisibleForTesting
-  void write(List<DuplicationsParser.Block> blocks, JsonWriter json, DbSession session) {
-    Map<String, String> refByComponentKey = newHashMap();
-    json.name("duplications").beginArray();
-    writeDuplications(blocks, refByComponentKey, json);
-    json.endArray();
-
-    json.name("files").beginObject();
-    writeFiles(refByComponentKey, json, session);
-    json.endObject();
-  }
-
-  private static void writeDuplications(List<DuplicationsParser.Block> blocks, Map<String, String> refByComponentKey, JsonWriter json) {
-    for (DuplicationsParser.Block block : blocks) {
-      json.beginObject().name("blocks").beginArray();
-      for (DuplicationsParser.Duplication duplication : block.getDuplications()) {
-        writeDuplication(refByComponentKey, duplication, json);
-      }
-      json.endArray().endObject();
-    }
-  }
-
-  private static void writeDuplication(Map<String, String> refByComponentKey, DuplicationsParser.Duplication duplication, JsonWriter json) {
-    String ref = null;
-    ComponentDto componentDto = duplication.file();
-    if (componentDto != null) {
-      String componentKey = componentDto.key();
-      ref = refByComponentKey.get(componentKey);
-      if (ref == null) {
-        ref = Integer.toString(refByComponentKey.size() + 1);
-        refByComponentKey.put(componentKey, ref);
-      }
-    }
-
-    json.beginObject();
-    json.prop("from", duplication.from());
-    json.prop("size", duplication.size());
-    json.prop("_ref", ref);
-    json.endObject();
-  }
-
-  private void writeFiles(Map<String, String> refByComponentKey, JsonWriter json, DbSession session) {
-    Map<String, ComponentDto> projectsByUuid = newHashMap();
-    Map<String, ComponentDto> parentProjectsByUuid = newHashMap();
-    for (Map.Entry<String, String> entry : refByComponentKey.entrySet()) {
-      String componentKey = entry.getKey();
-      String ref = entry.getValue();
-      Optional<ComponentDto> fileOptional = componentDao.selectByKey(session, componentKey);
-      if (fileOptional.isPresent()) {
-        ComponentDto file = fileOptional.get();
-        json.name(ref).beginObject();
-
-        addFile(json, file);
-        ComponentDto project = getProject(file.projectUuid(), projectsByUuid, session);
-        ComponentDto parentProject = getParentProject(file.getRootUuid(), parentProjectsByUuid, session);
-        addProject(json, project, parentProject);
-
-        json.endObject();
-      }
-    }
-  }
-
-  private static void addFile(JsonWriter json, ComponentDto file) {
-    json.prop("key", file.key());
-    json.prop("uuid", file.uuid());
-    json.prop("name", file.longName());
-  }
-
-  private static void addProject(JsonWriter json, @Nullable ComponentDto project, @Nullable ComponentDto subProject) {
-    if (project != null) {
-      json.prop("project", project.key());
-      json.prop("projectUuid", project.uuid());
-      json.prop("projectName", project.longName());
-
-      // Do not return sub project if sub project and project are the same
-      boolean displaySubProject = subProject != null && !subProject.uuid().equals(project.uuid());
-      if (displaySubProject) {
-        json.prop("subProject", subProject.key());
-        json.prop("subProjectUuid", subProject.uuid());
-        json.prop("subProjectName", subProject.longName());
-      }
-    }
-  }
-
-  private ComponentDto getProject(String projectUuid, Map<String, ComponentDto> projectsByUuid, DbSession session) {
-    ComponentDto project = projectsByUuid.get(projectUuid);
-    if (project == null) {
-      Optional<ComponentDto> projectOptional = componentDao.selectByUuid(session, projectUuid);
-      if (projectOptional.isPresent()) {
-        project = projectOptional.get();
-        projectsByUuid.put(project.uuid(), project);
-      }
-    }
-    return project;
-  }
-
-  private ComponentDto getParentProject(String rootUuid, Map<String, ComponentDto> subProjectsByUuid, DbSession session) {
-    ComponentDto project = subProjectsByUuid.get(rootUuid);
-    if (project == null) {
-      Optional<ComponentDto> projectOptional = componentDao.selectByUuid(session, rootUuid);
-      if (projectOptional.isPresent()) {
-        project = projectOptional.get();
-        subProjectsByUuid.put(project.uuid(), project);
-      }
-    }
-    return project;
-  }
-
-}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/duplication/ws/DuplicationsWsAction.java b/server/sonar-server/src/main/java/org/sonar/server/duplication/ws/DuplicationsWsAction.java
new file mode 100644 (file)
index 0000000..8c41d92
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.duplication.ws;
+
+import org.sonar.server.ws.WsAction;
+
+public interface DuplicationsWsAction extends WsAction {
+  // marker interface
+}
index 7e18c3df19ec2d6b476b0ba7465147788fbbb639..31e5fa771a000c4addf997ec8bfe035d60713bc9 100644 (file)
  */
 package org.sonar.server.duplication.ws;
 
-import com.google.common.io.Resources;
 import java.util.List;
-import java.util.Optional;
 import javax.annotation.CheckForNull;
 import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.server.ws.Change;
 import org.sonar.api.server.ws.Request;
-import org.sonar.api.server.ws.RequestHandler;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
-import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.api.web.UserRole;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
@@ -39,30 +36,34 @@ import org.sonar.server.component.ComponentFinder;
 import org.sonar.server.user.UserSession;
 
 import static org.sonar.server.component.ComponentFinder.ParamNames.UUID_AND_KEY;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
 
-public class ShowAction implements RequestHandler {
+public class ShowAction implements DuplicationsWsAction {
 
   private final DbClient dbClient;
   private final DuplicationsParser parser;
-  private final DuplicationsJsonWriter duplicationsJsonWriter;
+  private final ShowResponseBuilder responseBuilder;
   private final UserSession userSession;
   private final ComponentFinder componentFinder;
 
-  public ShowAction(DbClient dbClient, DuplicationsParser parser,
-    DuplicationsJsonWriter duplicationsJsonWriter, UserSession userSession, ComponentFinder componentFinder) {
+  public ShowAction(DbClient dbClient, DuplicationsParser parser, ShowResponseBuilder responseBuilder, UserSession userSession, ComponentFinder componentFinder) {
     this.dbClient = dbClient;
     this.parser = parser;
-    this.duplicationsJsonWriter = duplicationsJsonWriter;
+    this.responseBuilder = responseBuilder;
     this.userSession = userSession;
     this.componentFinder = componentFinder;
   }
 
-  void define(WebService.NewController controller) {
+  @Override
+  public void define(WebService.NewController controller) {
     WebService.NewAction action = controller.createAction("show")
       .setDescription("Get duplications. Require Browse permission on file's project")
       .setSince("4.4")
       .setHandler(this)
-      .setResponseExample(Resources.getResource(this.getClass(), "example-show.json"));
+      .setResponseExample(getClass().getResource("show-example.json"));
+
+    action.setChangelog(
+      new Change("6.5", "The fields 'uuid', 'projectUuid', 'subProjectUuid' are deprecated in the response."));
 
     action
       .createParam("key")
@@ -71,21 +72,20 @@ public class ShowAction implements RequestHandler {
 
     action
       .createParam("uuid")
-      .setDescription("File UUID")
+      .setDeprecatedSince("6.5")
+      .setDescription("File ID. If provided, 'key' must not be provided.")
       .setExampleValue("584a89f2-8037-4f7b-b82c-8b45d2d63fb2");
   }
 
   @Override
   public void handle(Request request, Response response) {
-    try (DbSession dbSession = dbClient.openSession(false);
-         JsonWriter json = response.newJsonWriter()) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
       ComponentDto component = componentFinder.getByUuidOrKey(dbSession, request.param("uuid"), request.param("key"), UUID_AND_KEY);
       userSession.checkComponentPermission(UserRole.CODEVIEWER, component);
-      json.beginObject();
       String duplications = findDataFromComponent(dbSession, component);
       List<DuplicationsParser.Block> blocks = parser.parse(component, duplications, dbSession);
-      duplicationsJsonWriter.write(blocks, json, dbSession);
-      json.endObject();
+
+      writeProtobuf(responseBuilder.build(blocks, dbSession), request, response);
     }
   }
 
@@ -95,7 +95,8 @@ public class ShowAction implements RequestHandler {
       .setComponentUuid(component.uuid())
       .setMetricKey(CoreMetrics.DUPLICATIONS_DATA_KEY)
       .build();
-    Optional<MeasureDto> measure = dbClient.measureDao().selectSingle(dbSession, query);
-    return measure.isPresent() ? measure.get().getData() : null;
+    return dbClient.measureDao().selectSingle(dbSession, query)
+      .map(MeasureDto::getData)
+      .orElse(null);
   }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/duplication/ws/ShowResponseBuilder.java b/server/sonar-server/src/main/java/org/sonar/server/duplication/ws/ShowResponseBuilder.java
new file mode 100644 (file)
index 0000000..9e61f60
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.duplication.ws;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDao;
+import org.sonar.db.component.ComponentDto;
+import org.sonarqube.ws.WsDuplications;
+import org.sonarqube.ws.WsDuplications.Block;
+import org.sonarqube.ws.WsDuplications.ShowResponse;
+
+import static com.google.common.collect.Maps.newHashMap;
+import static org.sonar.core.util.Protobuf.setNullable;
+
+public class ShowResponseBuilder {
+
+  private final ComponentDao componentDao;
+
+  public ShowResponseBuilder(DbClient dbClient) {
+    this.componentDao = dbClient.componentDao();
+  }
+
+  @VisibleForTesting
+  ShowResponseBuilder(ComponentDao componentDao) {
+    this.componentDao = componentDao;
+  }
+
+  ShowResponse build(List<DuplicationsParser.Block> blocks, DbSession session) {
+    ShowResponse.Builder response = ShowResponse.newBuilder();
+    Map<String, String> refByComponentKey = newHashMap();
+    blocks.stream()
+      .map(block -> toWsDuplication(block, refByComponentKey))
+      .forEach(response::addDuplications);
+
+    writeFiles(response, refByComponentKey, session);
+
+    return response.build();
+  }
+
+  private static WsDuplications.Duplication.Builder toWsDuplication(DuplicationsParser.Block block, Map<String, String> refByComponentKey) {
+    WsDuplications.Duplication.Builder wsDuplication = WsDuplications.Duplication.newBuilder();
+    block.getDuplications().stream()
+      .map(d -> toWsBlock(refByComponentKey, d))
+      .forEach(wsDuplication::addBlocks);
+
+    return wsDuplication;
+  }
+
+  private static Block.Builder toWsBlock(Map<String, String> refByComponentKey, DuplicationsParser.Duplication duplication) {
+    String ref = null;
+    ComponentDto componentDto = duplication.file();
+    if (componentDto != null) {
+      String componentKey = componentDto.key();
+      ref = refByComponentKey.get(componentKey);
+      if (ref == null) {
+        ref = Integer.toString(refByComponentKey.size() + 1);
+        refByComponentKey.put(componentKey, ref);
+      }
+    }
+
+    Block.Builder block = Block.newBuilder();
+    block.setFrom(duplication.from());
+    block.setSize(duplication.size());
+    setNullable(ref, block::setRef);
+
+    return block;
+  }
+
+  private static WsDuplications.File toWsFile(ComponentDto file, @Nullable ComponentDto project, @Nullable ComponentDto subProject) {
+    WsDuplications.File.Builder wsFile = WsDuplications.File.newBuilder();
+    wsFile.setKey(file.key());
+    wsFile.setUuid(file.uuid());
+    wsFile.setName(file.longName());
+
+    if (project != null) {
+      wsFile.setProject(project.key());
+      wsFile.setProjectUuid(project.uuid());
+      wsFile.setProjectName(project.longName());
+
+      // Do not return sub project if sub project and project are the same
+      boolean displaySubProject = subProject != null && !subProject.uuid().equals(project.uuid());
+      if (displaySubProject) {
+        wsFile.setSubProject(subProject.key());
+        wsFile.setSubProjectUuid(subProject.uuid());
+        wsFile.setSubProjectName(subProject.longName());
+      }
+    }
+
+    return wsFile.build();
+  }
+
+  private void writeFiles(ShowResponse.Builder response, Map<String, String> refByComponentKey, DbSession session) {
+    Map<String, ComponentDto> projectsByUuid = newHashMap();
+    Map<String, ComponentDto> parentModulesByUuid = newHashMap();
+    Map<String, WsDuplications.File> filesByRef = response.getMutableFiles();
+
+    for (Map.Entry<String, String> entry : refByComponentKey.entrySet()) {
+      String componentKey = entry.getKey();
+      String ref = entry.getValue();
+      Optional<ComponentDto> fileOptional = componentDao.selectByKey(session, componentKey);
+      if (fileOptional.isPresent()) {
+        ComponentDto file = fileOptional.get();
+
+        ComponentDto project = getProject(file.projectUuid(), projectsByUuid, session);
+        ComponentDto parentModule = getParentProject(file.getRootUuid(), parentModulesByUuid, session);
+        filesByRef.put(ref, toWsFile(file, project, parentModule));
+      }
+    }
+  }
+
+  private ComponentDto getProject(String projectUuid, Map<String, ComponentDto> projectsByUuid, DbSession session) {
+    ComponentDto project = projectsByUuid.get(projectUuid);
+    if (project == null) {
+      Optional<ComponentDto> projectOptional = componentDao.selectByUuid(session, projectUuid);
+      if (projectOptional.isPresent()) {
+        project = projectOptional.get();
+        projectsByUuid.put(project.uuid(), project);
+      }
+    }
+    return project;
+  }
+
+  private ComponentDto getParentProject(String rootUuid, Map<String, ComponentDto> subProjectsByUuid, DbSession session) {
+    ComponentDto project = subProjectsByUuid.get(rootUuid);
+    if (project == null) {
+      Optional<ComponentDto> projectOptional = componentDao.selectByUuid(session, rootUuid);
+      if (projectOptional.isPresent()) {
+        project = projectOptional.get();
+        subProjectsByUuid.put(project.uuid(), project);
+      }
+    }
+    return project;
+  }
+
+}
index 45722f00c6a7d3fcd0ab2809fa9f544c5c74afaa..abf786f89c0e1bb4d932ec89df2085a5a47b361e 100644 (file)
@@ -47,7 +47,7 @@ import org.sonar.server.component.ws.ComponentsWsModule;
 import org.sonar.server.debt.DebtModelPluginRepository;
 import org.sonar.server.debt.DebtModelXMLExporter;
 import org.sonar.server.debt.DebtRulesXMLImporter;
-import org.sonar.server.duplication.ws.DuplicationsJsonWriter;
+import org.sonar.server.duplication.ws.ShowResponseBuilder;
 import org.sonar.server.duplication.ws.DuplicationsParser;
 import org.sonar.server.duplication.ws.DuplicationsWs;
 import org.sonar.server.email.ws.EmailsWsModule;
@@ -430,7 +430,7 @@ public class PlatformLevel4 extends PlatformLevel {
       // Duplications
       DuplicationsParser.class,
       DuplicationsWs.class,
-      DuplicationsJsonWriter.class,
+      ShowResponseBuilder.class,
       org.sonar.server.duplication.ws.ShowAction.class,
 
       // text
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/duplication/ws/example-show.json b/server/sonar-server/src/main/resources/org/sonar/server/duplication/ws/example-show.json
deleted file mode 100644 (file)
index 16be68f..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-{
-  "duplications": [
-    {
-      "blocks": [
-        {
-          "from": 94, "size": 101, "_ref": "1"
-        },
-        {
-          "from": 83, "size": 101, "_ref": "2"
-        }
-      ]
-    },
-    {
-      "blocks": [
-        {
-          "from": 38, "size": 40, "_ref": "1"
-        },
-        {
-          "from": 29, "size": 39, "_ref": "2"
-        }
-      ]
-    },
-    {
-      "blocks": [
-        {
-          "from": 148, "size": 24, "_ref": "1"
-        },
-        {
-          "from": 137, "size": 24, "_ref": "2"
-        },
-        {
-          "from": 137, "size": 24, "_ref": "3"
-        }
-      ]
-    }
-  ],
-  "files": {
-    "1": {
-      "key": "org.codehaus.sonar:sonar-plugin-api:src/main/java/org/sonar/api/utils/command/CommandExecutor.java",
-      "name": "CommandExecutor",
-      "projectName": "SonarQube"
-    },
-    "2": {
-      "key": "com.sonarsource.orchestrator:sonar-orchestrator:src/main/java/com/sonar/orchestrator/util/CommandExecutor.java",
-      "name": "CommandExecutor",
-      "projectName": "SonarSource :: Orchestrator"
-    },
-    "3": {
-      "key": "org.codehaus.sonar.runner:sonar-runner-api:src/main/java/org/sonar/runner/api/CommandExecutor.java",
-      "name": "CommandExecutor",
-      "projectName": "SonarSource Runner"
-    }
-  }
-}
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/duplication/ws/show-example.json b/server/sonar-server/src/main/resources/org/sonar/server/duplication/ws/show-example.json
new file mode 100644 (file)
index 0000000..64f3fb7
--- /dev/null
@@ -0,0 +1,68 @@
+{
+  "duplications": [
+    {
+      "blocks": [
+        {
+          "from": 94,
+          "size": 101,
+          "_ref": "1"
+        },
+        {
+          "from": 83,
+          "size": 101,
+          "_ref": "2"
+        }
+      ]
+    },
+    {
+      "blocks": [
+        {
+          "from": 38,
+          "size": 40,
+          "_ref": "1"
+        },
+        {
+          "from": 29,
+          "size": 39,
+          "_ref": "2"
+        }
+      ]
+    },
+    {
+      "blocks": [
+        {
+          "from": 148,
+          "size": 24,
+          "_ref": "1"
+        },
+        {
+          "from": 137,
+          "size": 24,
+          "_ref": "2"
+        },
+        {
+          "from": 137,
+          "size": 24,
+          "_ref": "3"
+        }
+      ]
+    }
+  ],
+  "files": {
+    "1": {
+      "key": "org.codehaus.sonar:sonar-plugin-api:src/main/java/org/sonar/api/utils/command/CommandExecutor.java",
+      "name": "CommandExecutor",
+      "projectName": "SonarQube"
+    },
+    "2": {
+      "key": "com.sonarsource.orchestrator:sonar-orchestrator:src/main/java/com/sonar/orchestrator/util/CommandExecutor.java",
+      "name": "CommandExecutor",
+      "projectName": "SonarSource :: Orchestrator"
+    },
+    "3": {
+      "key": "org.codehaus.sonar.runner:sonar-runner-api:src/main/java/org/sonar/runner/api/CommandExecutor.java",
+      "name": "CommandExecutor",
+      "projectName": "SonarSource Runner"
+    }
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/duplication/ws/DuplicationsJsonWriterTest.java b/server/sonar-server/src/test/java/org/sonar/server/duplication/ws/DuplicationsJsonWriterTest.java
deleted file mode 100644 (file)
index d51dcc4..0000000
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.duplication.ws;
-
-import com.google.common.base.Optional;
-import java.io.StringWriter;
-import java.util.Collections;
-import java.util.List;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.runners.MockitoJUnitRunner;
-import org.sonar.api.utils.text.JsonWriter;
-import org.sonar.db.DbSession;
-import org.sonar.db.component.ComponentDao;
-import org.sonar.db.component.ComponentDto;
-import org.sonar.db.component.ComponentTesting;
-import org.sonar.db.organization.OrganizationTesting;
-import org.sonar.test.JsonAssert;
-
-import static com.google.common.collect.Lists.newArrayList;
-import static org.mockito.Matchers.anyLong;
-import static org.mockito.Mockito.anyString;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-@RunWith(MockitoJUnitRunner.class)
-public class DuplicationsJsonWriterTest {
-
-  @Mock
-  ComponentDao componentDao;
-
-  @Mock
-  DbSession session;
-
-  DuplicationsJsonWriter writer;
-
-  ComponentDto project;
-
-  @Before
-  public void setUp() {
-    project = ComponentTesting.newPrivateProjectDto(OrganizationTesting.newOrganizationDto())
-      .setId(1L)
-      .setName("SonarQube")
-      .setLongName("SonarQube")
-      .setKey("org.codehaus.sonar:sonar");
-
-    writer = new DuplicationsJsonWriter(componentDao);
-  }
-
-  @Test
-  public void write_duplications() {
-    String key1 = "org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyDeleteQuery.java";
-    ComponentDto file1 = ComponentTesting.newFileDto(project, null).setId(10L).setKey(key1).setLongName("PropertyDeleteQuery").setRootUuid("uuid_5");
-    String key2 = "org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyUpdateQuery.java";
-    ComponentDto file2 = ComponentTesting.newFileDto(project, null).setId(11L).setQualifier("FIL").setKey(key2).setLongName("PropertyUpdateQuery").setRootUuid("uuid_5");
-
-    when(componentDao.selectByKey(session, key1)).thenReturn(Optional.of(file1));
-    when(componentDao.selectByKey(session, key2)).thenReturn(Optional.of(file2));
-    when(componentDao.selectByUuid(session, "uuid_5")).thenReturn(Optional.of(
-      new ComponentDto().setUuid("uuid_5").setKey("org.codehaus.sonar:sonar-ws-client").setLongName("SonarQube :: Web Service Client")));
-    when(componentDao.selectByUuid(session, project.uuid())).thenReturn(Optional.of(project));
-
-    List<DuplicationsParser.Block> blocks = newArrayList();
-    blocks.add(new DuplicationsParser.Block(newArrayList(
-      new DuplicationsParser.Duplication(file1, 57, 12),
-      new DuplicationsParser.Duplication(file2, 73, 12)
-    )));
-
-    test(blocks,
-      "{\n" +
-        "  \"duplications\": [\n" +
-        "    {\n" +
-        "      \"blocks\": [\n" +
-        "        {\n" +
-        "          \"from\": 57, \"size\": 12, \"_ref\": \"1\"\n" +
-        "        },\n" +
-        "        {\n" +
-        "          \"from\": 73, \"size\": 12, \"_ref\": \"2\"\n" +
-        "        }\n" +
-        "      ]\n" +
-        "    }," +
-        "  ],\n" +
-        "  \"files\": {\n" +
-        "    \"1\": {\n" +
-        "      \"key\": \"org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyDeleteQuery.java\",\n" +
-        "      \"name\": \"PropertyDeleteQuery\",\n" +
-        "      \"project\": \"org.codehaus.sonar:sonar\",\n" +
-        "      \"projectName\": \"SonarQube\",\n" +
-        "      \"subProject\": \"org.codehaus.sonar:sonar-ws-client\",\n" +
-        "      \"subProjectName\": \"SonarQube :: Web Service Client\"\n" +
-        "    },\n" +
-        "    \"2\": {\n" +
-        "      \"key\": \"org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyUpdateQuery.java\",\n" +
-        "      \"name\": \"PropertyUpdateQuery\",\n" +
-        "      \"project\": \"org.codehaus.sonar:sonar\",\n" +
-        "      \"projectName\": \"SonarQube\",\n" +
-        "      \"subProject\": \"org.codehaus.sonar:sonar-ws-client\",\n" +
-        "      \"subProjectName\": \"SonarQube :: Web Service Client\"\n" +
-        "    }\n" +
-        "  }" +
-        "}");
-
-    verify(componentDao, times(2)).selectByKey(eq(session), anyString());
-    // Verify call to dao is cached when searching for project / sub project
-    verify(componentDao, times(1)).selectByUuid(eq(session), eq(project.uuid()));
-    verify(componentDao, times(1)).selectByUuid(eq(session), eq("uuid_5"));
-  }
-
-  @Test
-  public void write_duplications_without_sub_project() {
-    String key1 = "org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyDeleteQuery.java";
-    ComponentDto file1 = ComponentTesting.newFileDto(project, null).setId(10L).setKey(key1).setLongName("PropertyDeleteQuery");
-    String key2 = "org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyUpdateQuery.java";
-    ComponentDto file2 = ComponentTesting.newFileDto(project, null).setId(11L).setKey(key2).setLongName("PropertyUpdateQuery");
-
-    when(componentDao.selectByKey(session, key1)).thenReturn(Optional.of(file1));
-    when(componentDao.selectByKey(session, key2)).thenReturn(Optional.of(file2));
-    when(componentDao.selectById(eq(session), anyLong())).thenReturn(Optional.<ComponentDto>absent());
-    when(componentDao.selectByUuid(session, project.uuid())).thenReturn(Optional.of(project));
-
-    List<DuplicationsParser.Block> blocks = newArrayList();
-    blocks.add(new DuplicationsParser.Block(newArrayList(
-      new DuplicationsParser.Duplication(file1, 57, 12),
-      new DuplicationsParser.Duplication(file2, 73, 12)
-    )));
-
-    test(blocks,
-      "{\n" +
-        "  \"duplications\": [\n" +
-        "    {\n" +
-        "      \"blocks\": [\n" +
-        "        {\n" +
-        "          \"from\": 57, \"size\": 12, \"_ref\": \"1\"\n" +
-        "        },\n" +
-        "        {\n" +
-        "          \"from\": 73, \"size\": 12, \"_ref\": \"2\"\n" +
-        "        }\n" +
-        "      ]\n" +
-        "    }," +
-        "  ],\n" +
-        "  \"files\": {\n" +
-        "    \"1\": {\n" +
-        "      \"key\": \"org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyDeleteQuery.java\",\n" +
-        "      \"name\": \"PropertyDeleteQuery\",\n" +
-        "      \"project\": \"org.codehaus.sonar:sonar\",\n" +
-        "      \"projectName\": \"SonarQube\"\n" +
-        "    },\n" +
-        "    \"2\": {\n" +
-        "      \"key\": \"org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyUpdateQuery.java\",\n" +
-        "      \"name\": \"PropertyUpdateQuery\",\n" +
-        "      \"project\": \"org.codehaus.sonar:sonar\",\n" +
-        "      \"projectName\": \"SonarQube\"\n" +
-        "    }\n" +
-        "  }" +
-        "}");
-  }
-
-  @Test
-  public void write_duplications_with_a_removed_component() {
-    String key1 = "org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyDeleteQuery.java";
-    ComponentDto file1 = ComponentTesting.newFileDto(project, null).setId(10L).setKey(key1).setLongName("PropertyDeleteQuery");
-
-    when(componentDao.selectByKey(session, key1)).thenReturn(Optional.of(file1));
-    when(componentDao.selectByUuid(session, project.uuid())).thenReturn(Optional.of(project));
-    when(componentDao.selectById(eq(session), anyLong())).thenReturn(Optional.<ComponentDto>absent());
-
-    List<DuplicationsParser.Block> blocks = newArrayList();
-
-    blocks.add(new DuplicationsParser.Block(newArrayList(
-      new DuplicationsParser.Duplication(file1, 57, 12),
-      // Duplication on a removed file
-      new DuplicationsParser.Duplication(null, 73, 12)
-    )));
-
-    test(blocks,
-      "{\n" +
-        "  \"duplications\": [\n" +
-        "    {\n" +
-        "      \"blocks\": [\n" +
-        "        {\n" +
-        "          \"from\": 57, \"size\": 12, \"_ref\": \"1\"\n" +
-        "        },\n" +
-        "        {\n" +
-        "          \"from\": 73, \"size\": 12\n" +
-        "        }\n" +
-        "      ]\n" +
-        "    }," +
-        "  ],\n" +
-        "  \"files\": {\n" +
-        "    \"1\": {\n" +
-        "      \"key\": \"org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyDeleteQuery.java\",\n" +
-        "      \"name\": \"PropertyDeleteQuery\",\n" +
-        "      \"project\": \"org.codehaus.sonar:sonar\",\n" +
-        "      \"projectName\": \"SonarQube\"\n" +
-        "    }\n" +
-        "  }" +
-        "}");
-  }
-
-  @Test
-  public void write_nothing_when_no_data() {
-    test(Collections.<DuplicationsParser.Block>emptyList(), "{\"duplications\": [], \"files\": {}}");
-  }
-
-  private void test(List<DuplicationsParser.Block> blocks, String expected) {
-    StringWriter output = new StringWriter();
-    JsonWriter jsonWriter = JsonWriter.of(output);
-    jsonWriter.beginObject();
-    writer.write(blocks, jsonWriter, session);
-    jsonWriter.endObject();
-    JsonAssert.assertJson(output.toString()).isSimilarTo(expected);
-  }
-
-}
index 6dff1f318f310ecd79ac24434d5dd9e04c8d8bc1..8cf850d2547c3146ae18e0851539a5a66c49eecb 100644 (file)
@@ -35,7 +35,7 @@ public class DuplicationsWsTest {
   public UserSessionRule userSessionRule = UserSessionRule.standalone();
 
   WsTester tester = new WsTester(new DuplicationsWs(
-    new ShowAction(mock(DbClient.class), mock(DuplicationsParser.class), mock(DuplicationsJsonWriter.class), userSessionRule,
+    new ShowAction(mock(DbClient.class), mock(DuplicationsParser.class), mock(ShowResponseBuilder.class), userSessionRule,
       mock(ComponentFinder.class))));
 
   @Test
index 2300f6e3ff245a2b3fa1df7769ad9427b7ac8eb0..64774e7cc8fa0b4629601d8d9c1d2ae1b2ffa75a 100644 (file)
@@ -35,43 +35,46 @@ import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.startup.RegisterMetrics;
 import org.sonar.server.tester.UserSessionRule;
-import org.sonar.server.ws.WsTester;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.TestResponse;
+import org.sonar.server.ws.WsActionTester;
 
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.SnapshotTesting.newAnalysis;
 import static org.sonar.db.measure.MeasureTesting.newMeasureDto;
+import static org.sonar.test.JsonAssert.assertJson;
 
 public class ShowActionTest {
 
+  private static MetricDto dataMetric = RegisterMetrics.MetricToDto.INSTANCE.apply(CoreMetrics.DUPLICATIONS_DATA);
+
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
   @Rule
   public UserSessionRule userSessionRule = UserSessionRule.standalone();
+
   @Rule
   public DbTester db = DbTester.create();
-
   private DuplicationsParser parser = new DuplicationsParser(db.getDbClient().componentDao());
-  private DuplicationsJsonWriter duplicationsJsonWriter = new DuplicationsJsonWriter(db.getDbClient().componentDao());
-  private WsTester tester;
-  private MetricDto dataMetric = RegisterMetrics.MetricToDto.INSTANCE.apply(CoreMetrics.DUPLICATIONS_DATA);
+  private ShowResponseBuilder showResponseBuilder = new ShowResponseBuilder(db.getDbClient());
+
+  private WsActionTester ws = new WsActionTester(new ShowAction(db.getDbClient(), parser, showResponseBuilder, userSessionRule, TestComponentFinder.from(db)));
 
   @Before
   public void setUp() {
-    tester = new WsTester(new DuplicationsWs(new ShowAction(db.getDbClient(), parser, duplicationsJsonWriter, userSessionRule, TestComponentFinder.from(db))));
-
     db.getDbClient().metricDao().insert(db.getSession(), dataMetric);
     db.commit();
   }
 
   @Test
   public void get_duplications_by_file_key() throws Exception {
-    WsTester.TestRequest request = newBaseRequest();
+    TestRequest request = newBaseRequest();
     verifyCallToFileWithDuplications(file -> request.setParam("key", file.key()));
   }
 
   @Test
   public void get_duplications_by_file_id() throws Exception {
-    WsTester.TestRequest request = newBaseRequest();
+    TestRequest request = newBaseRequest();
     verifyCallToFileWithDuplications(file -> request.setParam("uuid", file.uuid()));
   }
 
@@ -83,23 +86,23 @@ public class ShowActionTest {
 
     userSessionRule.addProjectPermission(UserRole.CODEVIEWER, project);
 
-    WsTester.Result result = newBaseRequest().setParam("key", file.key()).execute();
+    TestResponse result = newBaseRequest().setParam("key", file.key()).execute();
 
-    result.assertJson("{\n" +
+    assertJson(result.getInput()).isSimilarTo("{\n" +
       "  \"duplications\": [],\n" +
       "  \"files\": {}\n" +
       "}");
   }
 
   @Test
-  public void return_404_if_file_does_not_exist() throws Exception {
+  public void fail_if_file_does_not_exist() throws Exception {
     expectedException.expect(NotFoundException.class);
 
     newBaseRequest().setParam("key", "missing").execute();
   }
 
   @Test
-  public void return_403_if_user_is_not_allowed_to_access_project() throws Exception {
+  public void fail_if_user_is_not_allowed_to_access_project() throws Exception {
     ComponentDto project = db.components().insertPrivateProject();
     ComponentDto file = db.components().insertComponent(newFileDto(project));
 
@@ -108,11 +111,19 @@ public class ShowActionTest {
     newBaseRequest().setParam("key", file.key()).execute();
   }
 
-  private WsTester.TestRequest newBaseRequest() {
-    return tester.newGetRequest("api/duplications", "show");
+  @Test
+  public void fail_if_no_parameter_provided() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Either 'uuid' or 'key' must be provided, not both");
+
+    newBaseRequest().execute();
+  }
+
+  private TestRequest newBaseRequest() {
+    return ws.newRequest();
   }
 
-  private void verifyCallToFileWithDuplications(Function<ComponentDto, WsTester.TestRequest> requestFactory) throws Exception {
+  private void verifyCallToFileWithDuplications(Function<ComponentDto, TestRequest> requestFactory) throws Exception {
     ComponentDto project = db.components().insertPrivateProject();
     ComponentDto file = db.components().insertComponent(newFileDto(project).setKey("foo.js"));
     SnapshotDto snapshot = db.components().insertSnapshot(newAnalysis(project));
@@ -127,10 +138,10 @@ public class ShowActionTest {
 
     userSessionRule.addProjectPermission(UserRole.CODEVIEWER, project);
 
-    WsTester.TestRequest request = requestFactory.apply(file);
-    WsTester.Result result = request.execute();
+    TestRequest request = requestFactory.apply(file);
+    TestResponse result = request.execute();
 
-    result.assertJson("{\"duplications\":[" +
+    assertJson(result.getInput()).isSimilarTo("{\"duplications\":[" +
       "{\"blocks\":[{\"from\":20,\"size\":5,\"_ref\":\"1\"},{\"from\":31,\"size\":5,\"_ref\":\"1\"}]}]," +
       "\"files\":{\"1\":{\"key\":\"foo.js\",\"uuid\":\"" + file.uuid() + "\"}}}");
   }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/duplication/ws/ShowResponseBuilderTest.java b/server/sonar-server/src/test/java/org/sonar/server/duplication/ws/ShowResponseBuilderTest.java
new file mode 100644 (file)
index 0000000..8dfc523
--- /dev/null
@@ -0,0 +1,224 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.duplication.ws;
+
+import java.io.StringWriter;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.core.util.ProtobufJsonFormat;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDao;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentTesting;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.organization.OrganizationTesting;
+import org.sonar.test.JsonAssert;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
+
+public class ShowResponseBuilderTest {
+
+  @Rule
+  public DbTester db = DbTester.create();
+  private DbSession dbSession = db.getSession();
+  private ComponentDao componentDao = spy(db.getDbClient().componentDao());
+
+  private ComponentDto project;
+  private OrganizationDto organization = OrganizationTesting.newOrganizationDto();
+
+  private ShowResponseBuilder underTest = new ShowResponseBuilder(componentDao);
+
+  @Before
+  public void setUp() {
+    project = newPrivateProjectDto(organization)
+      .setName("SonarQube")
+      .setLongName("SonarQube")
+      .setKey("org.codehaus.sonar:sonar");
+    db.components().insertComponent(project);
+  }
+
+  @Test
+  public void write_duplications() {
+    String key1 = "org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyDeleteQuery.java";
+    ComponentDto file1 = ComponentTesting.newFileDto(project, null).setKey(key1).setLongName("PropertyDeleteQuery").setRootUuid("uuid_5");
+    String key2 = "org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyUpdateQuery.java";
+    ComponentDto file2 = ComponentTesting.newFileDto(project, null).setQualifier("FIL").setKey(key2).setLongName("PropertyUpdateQuery").setRootUuid("uuid_5");
+    ComponentDto project2 = db.components().insertPrivateProject(organization, p -> p.setUuid("uuid_5").setKey("org.codehaus.sonar:sonar-ws-client")
+      .setLongName("SonarQube :: Web Service Client"));
+
+    db.components().insertComponent(file1);
+    db.components().insertComponent(file2);
+
+    List<DuplicationsParser.Block> blocks = newArrayList();
+    blocks.add(new DuplicationsParser.Block(newArrayList(
+      new DuplicationsParser.Duplication(file1, 57, 12),
+      new DuplicationsParser.Duplication(file2, 73, 12)
+    )));
+
+    test(blocks,
+      "{\n" +
+        "  \"duplications\": [\n" +
+        "    {\n" +
+        "      \"blocks\": [\n" +
+        "        {\n" +
+        "          \"from\": 57, \"size\": 12, \"_ref\": \"1\"\n" +
+        "        },\n" +
+        "        {\n" +
+        "          \"from\": 73, \"size\": 12, \"_ref\": \"2\"\n" +
+        "        }\n" +
+        "      ]\n" +
+        "    }," +
+        "  ],\n" +
+        "  \"files\": {\n" +
+        "    \"1\": {\n" +
+        "      \"key\": \"org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyDeleteQuery.java\",\n" +
+        "      \"name\": \"PropertyDeleteQuery\",\n" +
+        "      \"project\": \"org.codehaus.sonar:sonar\",\n" +
+        "      \"projectName\": \"SonarQube\",\n" +
+        "      \"subProject\": \"org.codehaus.sonar:sonar-ws-client\",\n" +
+        "      \"subProjectName\": \"SonarQube :: Web Service Client\"\n" +
+        "    },\n" +
+        "    \"2\": {\n" +
+        "      \"key\": \"org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyUpdateQuery.java\",\n" +
+        "      \"name\": \"PropertyUpdateQuery\",\n" +
+        "      \"project\": \"org.codehaus.sonar:sonar\",\n" +
+        "      \"projectName\": \"SonarQube\",\n" +
+        "      \"subProject\": \"org.codehaus.sonar:sonar-ws-client\",\n" +
+        "      \"subProjectName\": \"SonarQube :: Web Service Client\"\n" +
+        "    }\n" +
+        "  }" +
+        "}");
+
+    verify(componentDao, times(2)).selectByKey(eq(dbSession), anyString());
+    // Verify call to dao is cached when searching for project / sub project
+    verify(componentDao, times(1)).selectByUuid(eq(dbSession), eq(project.uuid()));
+    verify(componentDao, times(1)).selectByUuid(eq(dbSession), eq("uuid_5"));
+  }
+
+  @Test
+  public void write_duplications_without_sub_project() {
+    String key1 = "org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyDeleteQuery.java";
+    ComponentDto file1 = ComponentTesting.newFileDto(project, null).setId(10L).setKey(key1).setLongName("PropertyDeleteQuery");
+    String key2 = "org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyUpdateQuery.java";
+    ComponentDto file2 = ComponentTesting.newFileDto(project, null).setId(11L).setKey(key2).setLongName("PropertyUpdateQuery");
+
+    db.components().insertComponent(file1);
+    db.components().insertComponent(file2);
+
+    List<DuplicationsParser.Block> blocks = newArrayList();
+    blocks.add(new DuplicationsParser.Block(newArrayList(
+      new DuplicationsParser.Duplication(file1, 57, 12),
+      new DuplicationsParser.Duplication(file2, 73, 12)
+    )));
+
+    test(blocks,
+      "{\n" +
+        "  \"duplications\": [\n" +
+        "    {\n" +
+        "      \"blocks\": [\n" +
+        "        {\n" +
+        "          \"from\": 57, \"size\": 12, \"_ref\": \"1\"\n" +
+        "        },\n" +
+        "        {\n" +
+        "          \"from\": 73, \"size\": 12, \"_ref\": \"2\"\n" +
+        "        }\n" +
+        "      ]\n" +
+        "    }," +
+        "  ],\n" +
+        "  \"files\": {\n" +
+        "    \"1\": {\n" +
+        "      \"key\": \"org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyDeleteQuery.java\",\n" +
+        "      \"name\": \"PropertyDeleteQuery\",\n" +
+        "      \"project\": \"org.codehaus.sonar:sonar\",\n" +
+        "      \"projectName\": \"SonarQube\"\n" +
+        "    },\n" +
+        "    \"2\": {\n" +
+        "      \"key\": \"org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyUpdateQuery.java\",\n" +
+        "      \"name\": \"PropertyUpdateQuery\",\n" +
+        "      \"project\": \"org.codehaus.sonar:sonar\",\n" +
+        "      \"projectName\": \"SonarQube\"\n" +
+        "    }\n" +
+        "  }" +
+        "}");
+  }
+
+  @Test
+  public void write_duplications_with_a_removed_component() {
+    String key1 = "org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyDeleteQuery.java";
+    ComponentDto file1 = ComponentTesting.newFileDto(project, null).setId(10L).setKey(key1).setLongName("PropertyDeleteQuery");
+    db.components().insertComponent(file1);
+
+    List<DuplicationsParser.Block> blocks = newArrayList();
+
+    blocks.add(new DuplicationsParser.Block(newArrayList(
+      new DuplicationsParser.Duplication(file1, 57, 12),
+      // Duplication on a removed file
+      new DuplicationsParser.Duplication(null, 73, 12)
+    )));
+
+    test(blocks,
+      "{\n" +
+        "  \"duplications\": [\n" +
+        "    {\n" +
+        "      \"blocks\": [\n" +
+        "        {\n" +
+        "          \"from\": 57, \"size\": 12, \"_ref\": \"1\"\n" +
+        "        },\n" +
+        "        {\n" +
+        "          \"from\": 73, \"size\": 12\n" +
+        "        }\n" +
+        "      ]\n" +
+        "    }," +
+        "  ],\n" +
+        "  \"files\": {\n" +
+        "    \"1\": {\n" +
+        "      \"key\": \"org.codehaus.sonar:sonar-ws-client:src/main/java/org/sonar/wsclient/services/PropertyDeleteQuery.java\",\n" +
+        "      \"name\": \"PropertyDeleteQuery\",\n" +
+        "      \"project\": \"org.codehaus.sonar:sonar\",\n" +
+        "      \"projectName\": \"SonarQube\"\n" +
+        "    }\n" +
+        "  }" +
+        "}");
+  }
+
+  @Test
+  public void write_nothing_when_no_data() {
+    test(Collections.emptyList(), "{\"duplications\": [], \"files\": {}}");
+  }
+
+  private void test(List<DuplicationsParser.Block> blocks, String expected) {
+    StringWriter output = new StringWriter();
+    JsonWriter jsonWriter = JsonWriter.of(output);
+    ProtobufJsonFormat.write(underTest.build(blocks, dbSession), jsonWriter);
+    JsonAssert.assertJson(output.toString()).isSimilarTo(expected);
+  }
+
+}
diff --git a/sonar-ws/src/main/protobuf/ws-duplications.proto b/sonar-ws/src/main/protobuf/ws-duplications.proto
new file mode 100644 (file)
index 0000000..f8b292e
--- /dev/null
@@ -0,0 +1,54 @@
+// SonarQube, open source software quality management tool.
+// Copyright (C) 2008-2016 SonarSource
+// mailto:contact AT sonarsource DOT com
+//
+// SonarQube 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.
+//
+// SonarQube 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.
+
+syntax = "proto3";
+
+package sonarqube.ws.duplication;
+
+option java_package = "org.sonarqube.ws";
+option java_outer_classname = "WsDuplications";
+option optimize_for = SPEED;
+
+// WS api/duplications/show
+message ShowResponse {
+  repeated Duplication duplications = 1;
+  map<string,File> files = 2;
+
+}
+
+message Duplication {
+  repeated Block blocks = 1;
+}
+
+message Block {
+  int32 from = 1;
+  int32 size = 2;
+  string _ref = 3;
+}
+
+message File {
+  string key = 1;
+  string name = 2;
+  string uuid = 3;
+  string project = 4;
+  string projectUuid = 5;
+  string projectName = 6;
+  string subProject = 7;
+  string subProjectUuid = 8;
+  string subProjectName = 9;
+}