diff options
Diffstat (limited to 'server')
7 files changed, 471 insertions, 10 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWsModule.java index 2b6896679dc..e9fd3333ada 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWsModule.java @@ -33,6 +33,7 @@ public class ComponentsWsModule extends Module { AppAction.class, SearchAction.class, TreeAction.class, + ShowAction.class, SearchViewComponentsAction.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/ShowAction.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/ShowAction.java new file mode 100644 index 00000000000..dfc19ab3c40 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/ShowAction.java @@ -0,0 +1,160 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 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. + */ + +package org.sonar.server.component.ws; + +import java.util.List; +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.web.UserRole; +import org.sonar.core.permission.GlobalPermissions; +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.server.component.ComponentFinder; +import org.sonar.server.component.ComponentFinder.ParamNames; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.WsComponents; +import org.sonarqube.ws.WsComponents.ShowWsResponse; +import org.sonarqube.ws.client.component.ShowWsRequest; + +import static com.google.common.base.Objects.firstNonNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; +import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException; +import static org.sonar.server.ws.WsUtils.writeProtobuf; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.ACTION_SHOW; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_ID; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_KEY; + +public class ShowAction implements ComponentsWsAction { + private final UserSession userSession; + private final DbClient dbClient; + private final ComponentFinder componentFinder; + + public ShowAction(UserSession userSession, DbClient dbClient, ComponentFinder componentFinder) { + this.userSession = userSession; + this.dbClient = dbClient; + this.componentFinder = componentFinder; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction(ACTION_SHOW) + .setDescription(format("Returns a component (file, directory, project, view…) and its ancestors. " + + "The ancestors are ordered from the parent to the root project. " + + "The '%s' or '%s' must be provided.<br>" + + "Requires one of the following permissions:" + + "<ul>" + + "<li>'Administer System'</li>" + + "<li>'Administer' rights on the specified project</li>" + + "<li>'Browse' on the specified project</li>" + + "</ul>", + PARAM_ID, PARAM_KEY)) + .setResponseExample(getClass().getResource("show-example.json")) + .setSince("5.4") + .setHandler(this); + + action.createParam(PARAM_ID) + .setDescription("Component id") + .setExampleValue(UUID_EXAMPLE_01); + + action.createParam(PARAM_KEY) + .setDescription("Component key") + .setExampleValue("net.java.openjdk:jdk7"); + } + + @Override + public void handle(Request request, Response response) throws Exception { + ShowWsRequest showWsRequest = toShowWsRequest(request); + ShowWsResponse showWsResponse = doHandle(showWsRequest); + + writeProtobuf(showWsResponse, request, response); + } + + private ShowWsResponse doHandle(ShowWsRequest request) { + DbSession dbSession = dbClient.openSession(false); + try { + ComponentDto component = getComponentByUuidOrKey(dbSession, request); + SnapshotDto lastSnapshot = dbClient.snapshotDao().selectLastSnapshotByComponentId(dbSession, component.getId()); + List<ComponentDto> orderedAncestors = emptyList(); + if (lastSnapshot != null) { + ShowData.Builder showDataBuilder = ShowData.builder(lastSnapshot); + List<SnapshotDto> ancestorsSnapshots = dbClient.snapshotDao().selectByIds(dbSession, showDataBuilder.getOrderedSnapshotIds()); + showDataBuilder.withAncestorsSnapshots(ancestorsSnapshots); + List<ComponentDto> ancestorComponents = dbClient.componentDao().selectByIds(dbSession, showDataBuilder.getOrderedComponentIds()); + ShowData showData = showDataBuilder.andAncestorComponents(ancestorComponents); + orderedAncestors = showData.getComponents(); + } + + return buildResponse(component, orderedAncestors); + } finally { + dbClient.closeSession(dbSession); + } + } + + private static ShowWsResponse buildResponse(ComponentDto component, List<ComponentDto> orderedAncestorComponents) { + ShowWsResponse.Builder response = ShowWsResponse.newBuilder(); + response.setComponent(componentDtoToWsComponent(component)); + + for (ComponentDto ancestor : orderedAncestorComponents) { + response.addAncestors(componentDtoToWsComponent(ancestor)); + } + + return response.build(); + } + + private static WsComponents.Component.Builder componentDtoToWsComponent(ComponentDto dto) { + WsComponents.Component.Builder wsComponent = WsComponents.Component.newBuilder() + .setId(dto.uuid()) + .setKey(dto.key()) + .setName(dto.name()) + .setQualifier(dto.qualifier()); + if (!isNullOrEmpty(dto.path())) { + wsComponent.setPath(dto.path()); + } + if (!isNullOrEmpty(dto.description())) { + wsComponent.setDescription(dto.description()); + } + + return wsComponent; + } + + private ComponentDto getComponentByUuidOrKey(DbSession dbSession, ShowWsRequest request) { + ComponentDto component = componentFinder.getByUuidOrKey(dbSession, request.getId(), request.getKey(), ParamNames.ID_AND_KEY); + String projectUuid = firstNonNull(component.projectUuid(), component.uuid()); + if (!userSession.hasGlobalPermission(GlobalPermissions.SYSTEM_ADMIN) && + !userSession.hasProjectPermissionByUuid(UserRole.ADMIN, projectUuid) && + !userSession.hasProjectPermissionByUuid(UserRole.USER, projectUuid)) { + throw insufficientPrivilegesException(); + } + return component; + } + + private static ShowWsRequest toShowWsRequest(Request request) { + return new ShowWsRequest() + .setId(request.param(PARAM_ID)) + .setKey(request.param(PARAM_KEY)); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/ShowData.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/ShowData.java new file mode 100644 index 00000000000..6e7d447e414 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/ShowData.java @@ -0,0 +1,104 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 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. + */ + +package org.sonar.server.component.ws; + +import com.google.common.base.Function; +import com.google.common.base.Splitter; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; +import java.util.List; +import javax.annotation.Nonnull; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentDtoFunctions; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.component.SnapshotDtoFunctions; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +class ShowData { + private final List<ComponentDto> components; + + private ShowData(List<ComponentDto> components) { + this.components = components; + } + + static Builder builder(SnapshotDto snapshot) { + return new Builder(snapshot); + } + + List<ComponentDto> getComponents() { + return components; + } + + static class Builder { + private Ordering<SnapshotDto> snapshotOrdering; + private List<Long> orderedSnapshotIds; + private List<Long> orderedComponentIds; + + private Builder(SnapshotDto snapshot) { + List<String> orderedSnapshotIdsAsString = Splitter.on(".").omitEmptyStrings().splitToList(snapshot.getPath()); + orderedSnapshotIds = Lists.transform(orderedSnapshotIdsAsString, StringToLongFunction.INSTANCE); + snapshotOrdering = Ordering + .explicit(orderedSnapshotIds) + .onResultOf(SnapshotDtoFunctions.toId()) + .reverse(); + } + + Builder withAncestorsSnapshots(List<SnapshotDto> ancestorsSnapshots) { + checkNotNull(snapshotOrdering, "Snapshot must be set before the ancestors"); + checkState(orderedSnapshotIds.size() == ancestorsSnapshots.size(), "Missing ancestor"); + + orderedComponentIds = Lists.transform( + snapshotOrdering.immutableSortedCopy(ancestorsSnapshots), + SnapshotDtoFunctions.toComponentId()); + + return this; + } + + ShowData andAncestorComponents(List<ComponentDto> ancestorComponents) { + checkNotNull(orderedComponentIds, "Snapshot ancestors must be set before the component ancestors"); + checkState(orderedComponentIds.size() == ancestorComponents.size(), "Missing ancestor"); + + return new ShowData(Ordering + .explicit(orderedComponentIds) + .onResultOf(ComponentDtoFunctions.toId()) + .immutableSortedCopy(ancestorComponents)); + } + + List<Long> getOrderedSnapshotIds() { + return orderedSnapshotIds; + } + + List<Long> getOrderedComponentIds() { + return orderedComponentIds; + } + } + + private enum StringToLongFunction implements Function<String, Long> { + INSTANCE; + + @Override + public Long apply(@Nonnull String input) { + return Long.parseLong(input); + } + } +} diff --git a/server/sonar-server/src/main/resources/org/sonar/server/component/ws/show-example.json b/server/sonar-server/src/main/resources/org/sonar/server/component/ws/show-example.json new file mode 100644 index 00000000000..72a9988f652 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/component/ws/show-example.json @@ -0,0 +1,25 @@ +{ + "component": { + "id": "AVIF-FffA3Ax6PH2efPD", + "key": "com.sonarsource:java-markdown:src/main/java/com/sonarsource/markdown/impl/Rule.java", + "name": "Rule.java", + "qualifier": "FIL", + "path": "src/main/java/com/sonarsource/markdown/impl/Rule.java" + }, + "ancestors": [ + { + "id": "AVIF-FfgA3Ax6PH2efPF", + "key": "com.sonarsource:java-markdown:src/main/java/com/sonarsource/markdown/impl", + "name": "src/main/java/com/sonarsource/markdown/impl", + "qualifier": "DIR", + "path": "src/main/java/com/sonarsource/markdown/impl" + }, + { + "id": "AVIF98jgA3Ax6PH2efOW", + "key": "com.sonarsource:java-markdown", + "name": "Java Markdown", + "description": "Java Markdown Project", + "qualifier": "TRK" + } + ] +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsModuleTest.java index 5e854bb1f5f..dd31885a80d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsModuleTest.java @@ -30,6 +30,6 @@ public class ComponentsWsModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new ComponentsWsModule().configure(container); - assertThat(container.size()).isEqualTo(7 + 2); + assertThat(container.size()).isEqualTo(8 + 2); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/ShowActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/ShowActionTest.java new file mode 100644 index 00000000000..5e9500f4919 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/ShowActionTest.java @@ -0,0 +1,169 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 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. + */ + +package org.sonar.server.component.ws; + +import com.google.common.base.Throwables; +import java.io.IOException; +import java.io.InputStream; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.ExpectedException; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.utils.System2; +import org.sonar.api.web.UserRole; +import org.sonar.core.permission.GlobalPermissions; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.SnapshotDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; +import org.sonar.test.DbTests; +import org.sonarqube.ws.MediaTypes; +import org.sonarqube.ws.WsComponents.ShowWsResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.db.component.ComponentTesting.newDirectory; +import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonar.db.component.ComponentTesting.newProjectDto; +import static org.sonar.test.JsonAssert.assertJson; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_ID; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_KEY; + +@Category(DbTests.class) +public class ShowActionTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone() + .setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN); + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + ComponentDbTester componentDb = new ComponentDbTester(db); + + WsActionTester ws = new WsActionTester(new ShowAction(userSession, db.getDbClient(), new ComponentFinder(db.getDbClient()))); + + @Test + public void json_example() throws IOException { + insertJsonExampleComponentsAndSnapshots(); + + String response = ws.newRequest() + .setParam("id", "AVIF-FffA3Ax6PH2efPD") + .execute() + .getInput(); + + assertJson(response).isSimilarTo(getClass().getResource("show-example.json")); + } + + @Test + public void show_by_key_with_project_permission() { + userSession.anonymous().login().addProjectUuidPermissions(UserRole.ADMIN, "project-uuid"); + componentDb.insertProjectAndSnapshot(newProjectDto("project-uuid").setKey("project-key")); + + ShowWsResponse response = newRequest(null, "project-key"); + + assertThat(response.getAncestorsCount()).isEqualTo(0); + assertThat(response.getComponent().getKey()).isEqualTo("project-key"); + } + + @Test + public void show_with_browse_permission() { + userSession.anonymous().addProjectUuidPermissions(UserRole.USER, "project-uuid"); + componentDb.insertProjectAndSnapshot(newProjectDto("project-uuid")); + + ShowWsResponse response = newRequest("project-uuid", null); + + assertThat(response.getComponent().getId()).isEqualTo("project-uuid"); + } + + @Test + public void show_provided_project() { + componentDb.insertComponent(newProjectDto("project-uuid")); + + ShowWsResponse response = newRequest("project-uuid", null); + + assertThat(response.getComponent().getId()).isEqualTo("project-uuid"); + } + + @Test + public void fail_if_not_enough_privilege() { + userSession.anonymous().setGlobalPermissions(GlobalPermissions.QUALITY_PROFILE_ADMIN); + expectedException.expect(ForbiddenException.class); + componentDb.insertProjectAndSnapshot(newProjectDto("project-uuid")); + + newRequest("project-uuid", null); + } + + @Test + public void fail_if_component_does_not_exist() { + expectedException.expect(NotFoundException.class); + expectedException.expectMessage("Component id 'unknown-uuid' not found"); + + newRequest("unknown-uuid", null); + } + + private ShowWsResponse newRequest(@Nullable String uuid, @Nullable String key) { + TestRequest request = ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF); + + if (uuid != null) { + request.setParam(PARAM_ID, uuid); + } + if (key != null) { + request.setParam(PARAM_KEY, key); + } + + try(InputStream responseStream = request.execute().getInputStream()) { + return ShowWsResponse.parseFrom(responseStream); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private void insertJsonExampleComponentsAndSnapshots() { + ComponentDto project = newProjectDto("AVIF98jgA3Ax6PH2efOW") + .setKey("com.sonarsource:java-markdown") + .setName("Java Markdown") + .setDescription("Java Markdown Project") + .setQualifier(Qualifiers.PROJECT); + SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(project); + ComponentDto directory = newDirectory(project, "AVIF-FfgA3Ax6PH2efPF", "src/main/java/com/sonarsource/markdown/impl") + .setKey("com.sonarsource:java-markdown:src/main/java/com/sonarsource/markdown/impl") + .setName("src/main/java/com/sonarsource/markdown/impl") + .setQualifier(Qualifiers.DIRECTORY); + SnapshotDto directorySnapshot = componentDb.insertComponentAndSnapshot( + directory, + projectSnapshot); + componentDb.insertComponentAndSnapshot( + newFileDto(directory, "AVIF-FffA3Ax6PH2efPD") + .setKey("com.sonarsource:java-markdown:src/main/java/com/sonarsource/markdown/impl/Rule.java") + .setName("Rule.java") + .setPath("src/main/java/com/sonarsource/markdown/impl/Rule.java") + .setQualifier(Qualifiers.FILE), + directorySnapshot); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/TreeActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/TreeActionTest.java index cffa000bc2f..dc0cf55afe5 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/TreeActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/TreeActionTest.java @@ -28,6 +28,7 @@ import com.google.gson.JsonParser; import java.io.IOException; import java.io.InputStream; import java.util.Date; +import javax.annotation.CheckForNull; import org.apache.commons.io.IOUtils; import org.junit.Before; import org.junit.Rule; @@ -361,15 +362,15 @@ public class TreeActionTest { for (JsonElement componentAsJsonElement : components) { JsonObject componentAsJsonObject = componentAsJsonElement.getAsJsonObject(); componentDb.insertComponentAndSnapshot(new ComponentDto() - .setUuid(getJsonField(componentAsJsonObject, "id")) - .setKey(getJsonField(componentAsJsonObject, "key")) - .setName(getJsonField(componentAsJsonObject, "name")) - .setPath(getJsonField(componentAsJsonObject, "path")) - .setProjectUuid(project.projectUuid()) - .setQualifier(getJsonField(componentAsJsonObject, "qualifier")) - .setDescription(getJsonField(componentAsJsonObject, "description")) - .setEnabled(true) - .setCreatedAt(now), + .setUuid(getJsonField(componentAsJsonObject, "id")) + .setKey(getJsonField(componentAsJsonObject, "key")) + .setName(getJsonField(componentAsJsonObject, "name")) + .setPath(getJsonField(componentAsJsonObject, "path")) + .setProjectUuid(project.projectUuid()) + .setQualifier(getJsonField(componentAsJsonObject, "qualifier")) + .setDescription(getJsonField(componentAsJsonObject, "description")) + .setEnabled(true) + .setCreatedAt(now), projectSnapshot); } db.commit(); @@ -377,6 +378,7 @@ public class TreeActionTest { return project; } + @CheckForNull private static String getJsonField(JsonObject jsonObject, String field) { JsonElement jsonElement = jsonObject.get(field); return jsonElement == null ? null : jsonElement.getAsString(); |