From 000f6b88a4cb60410a2bad5ef976bb783b3c7ba9 Mon Sep 17 00:00:00 2001 From: Teryk Bellahsene Date: Wed, 3 Aug 2016 17:45:37 +0200 Subject: [PATCH] SONAR-7929 Create WS api/components/bulk_update_key --- .../java/it/component/ComponentsWsTest.java | 22 +- .../server/component/ComponentService.java | 26 +- .../component/ws/BulkUpdateKeyAction.java | 176 +++++++++++ .../component/ws/ComponentsWsModule.java | 3 +- .../component/ws/bulk_update_key-example.json | 19 ++ .../component/ComponentServiceTest.java | 89 ++---- .../ComponentServiceUpdateKeyTest.java | 2 +- .../component/ws/BulkUpdateKeyActionTest.java | 293 ++++++++++++++++++ .../component/ws/ComponentsWsModuleTest.java | 2 +- .../db/component/ResourceKeyUpdaterDao.java | 42 ++- .../component/ResourceKeyUpdaterDaoTest.java | 37 ++- .../client/component/BulkUpdateWsRequest.java | 111 +++++++ .../client/component/ComponentsService.java | 13 + .../component/ComponentsWsParameters.java | 3 + .../src/main/protobuf/ws-components.proto | 11 + 15 files changed, 758 insertions(+), 91 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/component/ws/BulkUpdateKeyAction.java create mode 100644 server/sonar-server/src/main/resources/org/sonar/server/component/ws/bulk_update_key-example.json create mode 100644 server/sonar-server/src/test/java/org/sonar/server/component/ws/BulkUpdateKeyActionTest.java create mode 100644 sonar-ws/src/main/java/org/sonarqube/ws/client/component/BulkUpdateWsRequest.java diff --git a/it/it-tests/src/test/java/it/component/ComponentsWsTest.java b/it/it-tests/src/test/java/it/component/ComponentsWsTest.java index a137231dc8e..5cba1c67c30 100644 --- a/it/it-tests/src/test/java/it/component/ComponentsWsTest.java +++ b/it/it-tests/src/test/java/it/component/ComponentsWsTest.java @@ -28,8 +28,9 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.sonarqube.ws.WsComponents; -import org.sonarqube.ws.client.HttpException; +import org.sonarqube.ws.WsComponents.BulkUpdateKeyWsResponse.Key; import org.sonarqube.ws.client.WsClient; +import org.sonarqube.ws.client.component.BulkUpdateWsRequest; import org.sonarqube.ws.client.component.SearchWsRequest; import org.sonarqube.ws.client.component.ShowWsRequest; import org.sonarqube.ws.client.component.UpdateWsRequest; @@ -89,4 +90,23 @@ public class ComponentsWsTest { assertThat(wsClient.components().show(new ShowWsRequest().setId(project.getId())).getComponent().getKey()).isEqualTo(newProjectKey); } + + @Test + public void bulk_update_key() { + String newProjectKey = "another_project_key"; + WsComponents.Component project = wsClient.components().show(new ShowWsRequest().setKey(PROJECT_KEY)).getComponent(); + assertThat(project.getKey()).isEqualTo(PROJECT_KEY); + + WsComponents.BulkUpdateKeyWsResponse result = wsClient.components().bulkUpdateKey(BulkUpdateWsRequest.builder() + .setKey(PROJECT_KEY) + .setFrom(PROJECT_KEY) + .setTo(newProjectKey) + .build()); + + assertThat(wsClient.components().show(new ShowWsRequest().setId(project.getId())).getComponent().getKey()).isEqualTo(newProjectKey); + assertThat(result.getKeysCount()).isEqualTo(1); + assertThat(result.getKeys(0)) + .extracting(Key::getKey, Key::getNewKey, Key::getDuplicate) + .containsOnlyOnce(PROJECT_KEY, newProjectKey, false); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ComponentService.java b/server/sonar-server/src/main/java/org/sonar/server/component/ComponentService.java index ae1c6297f6c..d2c3bd8b730 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ComponentService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ComponentService.java @@ -22,7 +22,6 @@ package org.sonar.server.component; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.collect.Collections2; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import java.util.Collection; import java.util.Date; @@ -34,7 +33,6 @@ import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.ce.ComputeEngineSide; import org.sonar.api.i18n.I18n; -import org.sonar.api.resources.Qualifiers; import org.sonar.api.resources.Scopes; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.System2; @@ -52,13 +50,12 @@ import org.sonar.server.user.UserSession; import static com.google.common.collect.Lists.newArrayList; import static org.sonar.core.component.ComponentKeys.isValidModuleKey; import static org.sonar.db.component.ComponentDtoFunctions.toKey; +import static org.sonar.db.component.ComponentKeyUpdaterDao.checkIsProjectOrModule; import static org.sonar.server.ws.WsUtils.checkRequest; @ServerSide @ComputeEngineSide public class ComponentService { - private static final Set ACCEPTED_QUALIFIERS = ImmutableSet.of(Qualifiers.PROJECT, Qualifiers.MODULE); - private final DbClient dbClient; private final I18n i18n; private final UserSession userSession; @@ -140,16 +137,21 @@ public class ComponentService { } } + public void bulkUpdateKey(DbSession dbSession, String projectKey, String stringToReplace, String replacementString) { + ComponentDto project = getByKey(dbSession, projectKey); + userSession.checkComponentUuidPermission(UserRole.ADMIN, project.projectUuid()); + checkIsProjectOrModule(project); + dbClient.resourceKeyUpdaterDao().bulkUpdateKey(dbSession, project.uuid(), stringToReplace, replacementString); + } + public void bulkUpdateKey(String projectKey, String stringToReplace, String replacementString) { // Open a batch session - DbSession session = dbClient.openSession(true); + DbSession dbSession = dbClient.openSession(true); try { - ComponentDto project = getByKey(session, projectKey); - userSession.checkComponentUuidPermission(UserRole.ADMIN, project.projectUuid()); - dbClient.resourceKeyUpdaterDao().bulkUpdateKey(session, project.uuid(), stringToReplace, replacementString); - session.commit(); + bulkUpdateKey(dbSession, projectKey, stringToReplace, replacementString); + dbSession.commit(); } finally { - session.close(); + dbSession.close(); } } @@ -301,8 +303,4 @@ public class ComponentService { private ComponentDto getByKey(DbSession session, String key) { return componentFinder.getByKey(session, key); } - - private static void checkIsProjectOrModule(ComponentDto component) { - checkRequest(ACCEPTED_QUALIFIERS.contains(component.qualifier()), "Component updated must be a module or a key"); - } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/BulkUpdateKeyAction.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/BulkUpdateKeyAction.java new file mode 100644 index 00000000000..fe403a77f2a --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/BulkUpdateKeyAction.java @@ -0,0 +1,176 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.component.ws; + +import com.google.common.collect.ImmutableList; +import java.util.Map; +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.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentKeyUpdaterDao; +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.BulkUpdateKeyWsResponse; +import org.sonarqube.ws.client.component.BulkUpdateWsRequest; + +import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; +import static org.sonar.db.component.ComponentKeyUpdaterDao.checkIsProjectOrModule; +import static org.sonar.server.ws.WsUtils.checkRequest; +import static org.sonar.server.ws.WsUtils.writeProtobuf; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_DRY_RUN; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_FROM; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_ID; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_KEY; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_TO; + +public class BulkUpdateKeyAction implements ComponentsWsAction { + private final DbClient dbClient; + private final ComponentFinder componentFinder; + private final ComponentKeyUpdaterDao componentKeyUpdater; + private final UserSession userSession; + + public BulkUpdateKeyAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { + this.dbClient = dbClient; + this.componentKeyUpdater = dbClient.resourceKeyUpdaterDao(); + this.componentFinder = componentFinder; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("bulk_update_key") + .setDescription("Bulk update a project or module key and all its sub-components keys. " + + "The bulk update allows to replace a part of the current key by another string on the current project and all its sub-modules.
" + + "It's possible to simulate the bulk update by setting the parameter '%s' at true. No key is updated with a dry run.
" + + "Ex: to rename a project with key 'my_project' to 'my_new_project' and all its sub-components keys, call the WS with parameters:" + + "" + + "Either '%s' or '%s' must be provided, not both.
" + + "Requires one of the following permissions: " + + "", + PARAM_DRY_RUN, + PARAM_KEY, PARAM_FROM, PARAM_TO, + PARAM_ID, PARAM_KEY) + .setSince("6.1") + .setPost(true) + .setResponseExample(getClass().getResource("bulk_update_key-example.json")) + .setHandler(this); + + action.createParam(PARAM_ID) + .setDescription("Project or module id") + .setExampleValue(UUID_EXAMPLE_01); + + action.createParam(PARAM_KEY) + .setDescription("Project or module key") + .setExampleValue("my_old_project"); + + action.createParam(PARAM_FROM) + .setDescription("String to match in components keys") + .setRequired(true) + .setExampleValue("_old"); + + action.createParam(PARAM_TO) + .setDescription("String replacement in components keys") + .setRequired(true) + .setExampleValue("_new"); + + action.createParam(PARAM_DRY_RUN) + .setDescription("Simulate bulk update. No component key is updated.") + .setBooleanPossibleValues() + .setDefaultValue(false); + } + + @Override + public void handle(Request request, Response response) throws Exception { + writeProtobuf(doHandle(toWsRequest(request)), request, response); + } + + private BulkUpdateKeyWsResponse doHandle(BulkUpdateWsRequest request) { + DbSession dbSession = dbClient.openSession(false); + try { + ComponentDto projectOrModule = componentFinder.getByUuidOrKey(dbSession, request.getId(), request.getKey(), ParamNames.ID_AND_KEY); + checkIsProjectOrModule(projectOrModule); + userSession.checkComponentUuidPermission(UserRole.ADMIN, projectOrModule.uuid()); + + Map newKeysByOldKeys = componentKeyUpdater.simulateBulkUpdateKey(dbSession, projectOrModule.uuid(), request.getFrom(), request.getTo()); + Map newKeysWithDuplicateMap = componentKeyUpdater.checkComponentKeys(dbSession, ImmutableList.copyOf(newKeysByOldKeys.values())); + + if (!request.isDryRun()) { + checkNoDuplicate(newKeysWithDuplicateMap); + bulkUpdateKey(dbSession, request, projectOrModule); + } + + return buildResponse(newKeysByOldKeys, newKeysWithDuplicateMap); + } finally { + dbClient.closeSession(dbSession); + } + } + + private static void checkNoDuplicate(Map newKeysWithDuplicateMap) { + newKeysWithDuplicateMap.entrySet().forEach(entry -> checkRequest(!entry.getValue(), "Impossible to update key: a component with key \"%s\" already exists.", entry.getKey())); + } + + private void bulkUpdateKey(DbSession dbSession, BulkUpdateWsRequest request, ComponentDto projectOrModule) { + componentKeyUpdater.bulkUpdateKey(dbSession, projectOrModule.uuid(), request.getFrom(), request.getTo()); + dbSession.commit(); + } + + private static BulkUpdateKeyWsResponse buildResponse(Map newKeysByOldKeys, Map newKeysWithDuplicateMap) { + WsComponents.BulkUpdateKeyWsResponse.Builder response = WsComponents.BulkUpdateKeyWsResponse.newBuilder(); + + newKeysByOldKeys.entrySet().stream() + // sort by old key + .sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey())) + .forEach( + entry -> { + String newKey = entry.getValue(); + response.addKeysBuilder() + .setKey(entry.getKey()) + .setNewKey(newKey) + .setDuplicate(newKeysWithDuplicateMap.getOrDefault(newKey, false)); + }); + + return response.build(); + } + + private static BulkUpdateWsRequest toWsRequest(Request request) { + return BulkUpdateWsRequest.builder() + .setId(request.param(PARAM_ID)) + .setKey(request.param(PARAM_KEY)) + .setFrom(request.mandatoryParam(PARAM_FROM)) + .setTo(request.mandatoryParam(PARAM_TO)) + .setDryRun(request.mandatoryParamAsBoolean(PARAM_DRY_RUN)) + .build(); + } +} 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 71f711d068b..425d7917d01 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 @@ -34,6 +34,7 @@ public class ComponentsWsModule extends Module { TreeAction.class, ShowAction.class, SearchViewComponentsAction.class, - UpdateKeyAction.class); + UpdateKeyAction.class, + BulkUpdateKeyAction.class); } } diff --git a/server/sonar-server/src/main/resources/org/sonar/server/component/ws/bulk_update_key-example.json b/server/sonar-server/src/main/resources/org/sonar/server/component/ws/bulk_update_key-example.json new file mode 100644 index 00000000000..7ad4b3d8a2c --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/component/ws/bulk_update_key-example.json @@ -0,0 +1,19 @@ +{ + "keys": [ + { + "key": "my_project", + "newKey": "my_new_project", + "duplicate": false + }, + { + "key": "my_project:module_1", + "newKey": "my_new_project:module_1", + "duplicate": true + }, + { + "key": "my_project:module_2", + "newKey": "my_new_project:module_2", + "duplicate": false + } + ] +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceTest.java index 45b104da4e0..5c3f5ee58f0 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceTest.java @@ -30,10 +30,12 @@ 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.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDao; +import org.sonar.db.component.ComponentDbTester; import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentTesting; import org.sonar.db.component.ResourceIndexDao; @@ -53,6 +55,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.sonar.core.permission.GlobalPermissions.PROVISIONING; +import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonar.db.component.ComponentTesting.newProjectDto; public class ComponentServiceTest { @@ -63,6 +67,7 @@ public class ComponentServiceTest { @Rule public DbTester dbTester = DbTester.create(System2.INSTANCE); + ComponentDbTester componentDb = new ComponentDbTester(dbTester); DbClient dbClient = dbTester.getDbClient(); DbSession dbSession = dbTester.getSession(); @@ -79,37 +84,37 @@ public class ComponentServiceTest { @Test public void get_by_key() { - ComponentDto project = createProject(); + ComponentDto project = insertSampleProject(); assertThat(underTest.getByKey(project.getKey())).isNotNull(); } @Test public void get_nullable_by_key() { - ComponentDto project = createProject(); + ComponentDto project = insertSampleProject(); assertThat(underTest.getNullableByKey(project.getKey())).isNotNull(); assertThat(underTest.getNullableByKey("unknown")).isNull(); } @Test public void get_by_uuid() { - ComponentDto project = createProject(); + ComponentDto project = insertSampleProject(); assertThat(underTest.getNonNullByUuid(project.uuid())).isNotNull(); } @Test public void get_nullable_by_uuid() { - ComponentDto project = createProject(); + ComponentDto project = insertSampleProject(); assertThat(underTest.getByUuid(project.uuid())).isPresent(); assertThat(underTest.getByUuid("unknown")).isAbsent(); } @Test public void check_module_keys_before_renaming() { - ComponentDto project = createProject(); + ComponentDto project = insertSampleProject(); ComponentDto module = ComponentTesting.newModuleDto(project).setKey("sample:root:module"); dbClient.componentDao().insert(dbSession, module); - ComponentDto file = ComponentTesting.newFileDto(module).setKey("sample:root:module:src/File.xoo"); + ComponentDto file = newFileDto(module).setKey("sample:root:module:src/File.xoo"); dbClient.componentDao().insert(dbSession, file); dbSession.commit(); @@ -124,7 +129,7 @@ public class ComponentServiceTest { @Test public void check_module_keys_before_renaming_return_duplicate_key() { - ComponentDto project = createProject(); + ComponentDto project = insertSampleProject(); ComponentDto module = ComponentTesting.newModuleDto(project).setKey("sample:root:module"); dbClient.componentDao().insert(dbSession, module); @@ -145,65 +150,12 @@ public class ComponentServiceTest { public void fail_to_check_module_keys_before_renaming_without_admin_permission() { expectedException.expect(ForbiddenException.class); - ComponentDto project = createProject(); + ComponentDto project = insertSampleProject(); userSession.login("john").addProjectUuidPermissions(UserRole.USER, project.uuid()); underTest.checkModuleKeysBeforeRenaming(project.key(), "sample", "sample2"); } - @Test - public void bulk_update_project_key() { - ComponentDto project = createProject(); - ComponentDto module = ComponentTesting.newModuleDto(project).setKey("sample:root:module"); - dbClient.componentDao().insert(dbSession, module); - - ComponentDto file = ComponentTesting.newFileDto(module).setKey("sample:root:module:src/File.xoo"); - dbClient.componentDao().insert(dbSession, file); - - dbSession.commit(); - - userSession.login("john").addProjectUuidPermissions(UserRole.ADMIN, project.uuid()); - underTest.bulkUpdateKey(project.key(), "sample", "sample2"); - dbSession.commit(); - - // Check project key has been updated - assertThat(underTest.getNullableByKey(project.key())).isNull(); - assertThat(underTest.getNullableByKey("sample2:root")).isNotNull(); - - // Check module key has been updated - assertThat(underTest.getNullableByKey(module.key())).isNull(); - assertThat(underTest.getNullableByKey("sample2:root:module")).isNotNull(); - - // Check file key has been updated - assertThat(underTest.getNullableByKey(file.key())).isNull(); - assertThat(underTest.getNullableByKey("sample2:root:module:src/File.xoo")).isNotNull(); - } - - @Test - public void bulk_update_provisioned_project_key() { - ComponentDto provisionedProject = ComponentTesting.newProjectDto().setKey("provisionedProject"); - dbClient.componentDao().insert(dbSession, provisionedProject); - - dbSession.commit(); - - userSession.login("john").addProjectUuidPermissions(UserRole.ADMIN, provisionedProject.uuid()); - underTest.bulkUpdateKey(provisionedProject.key(), "provisionedProject", "provisionedProject2"); - dbSession.commit(); - - // Check project key has been updated - assertThat(underTest.getNullableByKey(provisionedProject.key())).isNull(); - assertThat(underTest.getNullableByKey("provisionedProject2")).isNotNull(); - } - - @Test - public void fail_to_bulk_update_project_key_without_admin_permission() { - expectedException.expect(ForbiddenException.class); - - ComponentDto project = createProject(); - userSession.login("john").addProjectPermissions(UserRole.USER, project.key()); - underTest.bulkUpdateKey("sample:root", "sample", "sample2"); - } - @Test public void create_project() { userSession.login("john").setGlobalPermissions(PROVISIONING); @@ -344,12 +296,12 @@ public class ComponentServiceTest { @Test public void should_return_project_uuids() { - ComponentDto project = createProject(); + ComponentDto project = insertSampleProject(); String moduleKey = "sample:root:module"; ComponentDto module = ComponentTesting.newModuleDto(project).setKey(moduleKey); dbClient.componentDao().insert(dbSession, module); String fileKey = "sample:root:module:Foo.xoo"; - ComponentDto file = ComponentTesting.newFileDto(module).setKey(fileKey); + ComponentDto file = newFileDto(module).setKey(fileKey); dbClient.componentDao().insert(dbSession, file); dbSession.commit(); @@ -379,11 +331,12 @@ public class ComponentServiceTest { assertThat(underTest.componentUuids(dbSession, Arrays.asList(moduleKey, fileKey), true)).isEmpty(); } - private ComponentDto createProject() { - ComponentDto project = ComponentTesting.newProjectDto().setKey("sample:root"); - dbClient.componentDao().insert(dbSession, project); - dbSession.commit(); - return project; + private ComponentDto insertSampleProject() { + return componentDb.insertComponent(newProjectDto().setKey("sample:root")); + } + + private void setGlobalAdminPermission() { + userSession.setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java index 1fbe57b1474..8c48413bffb 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java @@ -173,7 +173,7 @@ public class ComponentServiceUpdateKeyTest { ComponentDto project = insertSampleRootProject(); ComponentDto file = componentDb.insertComponent(newFileDto(project)); - expectedException.expect(BadRequestException.class); + expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Component updated must be a module or a key"); underTest.updateKey(dbSession, file.key(), "file:key"); diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/BulkUpdateKeyActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/BulkUpdateKeyActionTest.java new file mode 100644 index 00000000000..f01f11d6f51 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/BulkUpdateKeyActionTest.java @@ -0,0 +1,293 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.component.ws; + +import com.google.common.base.Throwables; +import java.io.IOException; +import javax.annotation.Nullable; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.System2; +import org.sonar.core.permission.GlobalPermissions; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.BadRequestException; +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.sonarqube.ws.MediaTypes; +import org.sonarqube.ws.WsComponents; +import org.sonarqube.ws.WsComponents.BulkUpdateKeyWsResponse; +import org.sonarqube.ws.WsComponents.BulkUpdateKeyWsResponse.Key; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.assertj.guava.api.Assertions.assertThat; +import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonar.db.component.ComponentTesting.newModuleDto; +import static org.sonar.db.component.ComponentTesting.newProjectDto; +import static org.sonar.test.JsonAssert.assertJson; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_DRY_RUN; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_FROM; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_ID; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_KEY; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_TO; + +public class BulkUpdateKeyActionTest { + static final String MY_PROJECT_KEY = "my_project"; + static final String FROM = "my_"; + static final String TO = "your_"; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + ComponentDbTester componentDb = new ComponentDbTester(db); + DbClient dbClient = db.getDbClient(); + DbSession dbSession = db.getSession(); + + ComponentFinder componentFinder = new ComponentFinder(dbClient); + + WsActionTester ws = new WsActionTester(new BulkUpdateKeyAction(dbClient, componentFinder, userSession)); + + @Before + public void setUp() { + userSession.setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN); + } + + @Test + public void json_example() { + ComponentDto project = componentDb.insertComponent(newProjectDto().setKey("my_project")); + componentDb.insertComponent(newModuleDto(project).setKey("my_project:module_1")); + ComponentDto anotherProject = componentDb.insertComponent(newProjectDto().setKey("another_project")); + componentDb.insertComponent(newModuleDto(anotherProject).setKey("my_new_project:module_1")); + ComponentDto module2 = componentDb.insertComponent(newModuleDto(project).setKey("my_project:module_2")); + componentDb.insertComponent(newFileDto(module2)); + + String result = ws.newRequest() + .setParam(PARAM_KEY, "my_project") + .setParam(PARAM_FROM, "my_") + .setParam(PARAM_TO, "my_new_") + .setParam(PARAM_DRY_RUN, String.valueOf(true)) + .execute().getInput(); + + assertJson(result).withStrictArrayOrder().isSimilarTo(getClass().getResource("bulk_update_key-example.json")); + } + + @Test + public void dry_run_by_key() { + insertMyProject(); + + BulkUpdateKeyWsResponse result = callDryRunByKey(MY_PROJECT_KEY, FROM, TO); + + assertThat(result.getKeysCount()).isEqualTo(1); + assertThat(result.getKeys(0).getNewKey()).isEqualTo("your_project"); + } + + @Test + public void bulk_update_project_key() { + ComponentDto project = insertMyProject(); + ComponentDto module = componentDb.insertComponent(newModuleDto(project).setKey("my_project:root:module")); + ComponentDto file = componentDb.insertComponent(newFileDto(module).setKey("my_project:root:module:src/File.xoo")); + + BulkUpdateKeyWsResponse result = callByUuid(project.uuid(), FROM, TO); + + assertThat(result.getKeysCount()).isEqualTo(2); + assertThat(result.getKeysList()).extracting(Key::getKey, Key::getNewKey, Key::getDuplicate) + .containsExactly( + tuple(project.key(), "your_project", false), + tuple(module.key(), "your_project:root:module", false)); + + assertComponentKeyUpdated(project.key(), "your_project"); + assertComponentKeyUpdated(module.key(), "your_project:root:module"); + assertComponentKeyUpdated(file.key(), "your_project:root:module:src/File.xoo"); + } + + @Test + public void bulk_update_provisioned_project_key() { + String oldKey = "provisionedProject"; + String newKey = "provisionedProject2"; + ComponentDto provisionedProject = componentDb.insertComponent(newProjectDto().setKey(oldKey)); + + callByKey(provisionedProject.key(), oldKey, newKey); + + assertComponentKeyUpdated(oldKey, newKey); + } + + @Test + public void fail_to_bulk_if_a_component_already_exists_with_the_same_key() { + componentDb.insertComponent(newProjectDto().setKey("my_project")); + componentDb.insertComponent(newProjectDto().setKey("your_project")); + + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("Impossible to update key: a component with key \"your_project\" already exists."); + + callByKey("my_project", "my_", "your_"); + } + + @Test + public void fail_to_bulk_update_with_invalid_new_key() { + insertMyProject(); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Malformed key for 'my?project'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit."); + + callByKey(MY_PROJECT_KEY, FROM, "my?"); + } + + @Test + public void fail_to_bulk_update_if_not_project_or_module() { + ComponentDto project = insertMyProject(); + ComponentDto file = componentDb.insertComponent(newFileDto(project)); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Component updated must be a module or a key"); + + callByKey(file.key(), FROM, TO); + } + + @Test + public void fail_if_from_string_is_not_provided() { + expectedException.expect(IllegalArgumentException.class); + + ComponentDto project = insertMyProject(); + + callDryRunByKey(project.key(), null, TO); + } + + @Test + public void fail_if_to_string_is_not_provided() { + expectedException.expect(IllegalArgumentException.class); + + ComponentDto project = insertMyProject(); + + callDryRunByKey(project.key(), FROM, null); + } + + @Test + public void fail_if_uuid_nor_key_provided() { + expectedException.expect(IllegalArgumentException.class); + + call(null, null, FROM, TO, false); + } + + @Test + public void fail_if_uuid_and_key_provided() { + expectedException.expect(IllegalArgumentException.class); + + ComponentDto project = insertMyProject(); + + call(project.uuid(), project.key(), FROM, TO, false); + } + + @Test + public void fail_if_project_does_not_exist() { + expectedException.expect(NotFoundException.class); + + callDryRunByUuid("UNKNOWN_UUID", FROM, TO); + } + + @Test + public void fail_if_insufficient_privileges() { + expectedException.expect(ForbiddenException.class); + userSession.anonymous(); + + ComponentDto project = insertMyProject(); + + callDryRunByUuid(project.uuid(), FROM, TO); + } + + @Test + public void api_definition() { + WebService.Action definition = ws.getDef(); + + assertThat(definition.isPost()).isTrue(); + assertThat(definition.since()).isEqualTo("6.1"); + assertThat(definition.key()).isEqualTo("bulk_update_key"); + assertThat(definition.params()) + .hasSize(5) + .extracting(WebService.Param::key) + .containsOnlyOnce("id", "key", "from", "to", "dryRun"); + } + + private void assertComponentKeyUpdated(String oldKey, String newKey) { + assertThat(dbClient.componentDao().selectByKey(dbSession, oldKey)).isAbsent(); + assertThat(dbClient.componentDao().selectByKey(dbSession, newKey)).isPresent(); + } + + private ComponentDto insertMyProject() { + return componentDb.insertComponent(newProjectDto().setKey(MY_PROJECT_KEY)); + } + + private WsComponents.BulkUpdateKeyWsResponse callDryRunByUuid(@Nullable String uuid, @Nullable String from, @Nullable String to) { + return call(uuid, null, from, to, true); + } + + private BulkUpdateKeyWsResponse callDryRunByKey(@Nullable String key, @Nullable String from, @Nullable String to) { + return call(null, key, from, to, true); + } + + private WsComponents.BulkUpdateKeyWsResponse callByUuid(@Nullable String uuid, @Nullable String from, @Nullable String to) { + return call(uuid, null, from, to, false); + } + + private BulkUpdateKeyWsResponse callByKey(@Nullable String key, @Nullable String from, @Nullable String to) { + return call(null, key, from, to, false); + } + + private BulkUpdateKeyWsResponse call(@Nullable String uuid, @Nullable String key, @Nullable String from, @Nullable String to, @Nullable Boolean dryRun) { + TestRequest request = ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF); + + if (uuid != null) { + request.setParam(PARAM_ID, uuid); + } + if (key != null) { + request.setParam(PARAM_KEY, key); + } + if (from != null) { + request.setParam(PARAM_FROM, from); + } + if (to != null) { + request.setParam(PARAM_TO, to); + } + if (dryRun != null) { + request.setParam(PARAM_DRY_RUN, String.valueOf(dryRun)); + } + + try { + return WsComponents.BulkUpdateKeyWsResponse.parseFrom(request.execute().getInputStream()); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } +} 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 2602ff9c008..7d5b4443f9c 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 @@ -29,6 +29,6 @@ public class ComponentsWsModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new ComponentsWsModule().configure(container); - assertThat(container.size()).isEqualTo(9 + 2); + assertThat(container.size()).isEqualTo(10 + 2); } } diff --git a/sonar-db/src/main/java/org/sonar/db/component/ResourceKeyUpdaterDao.java b/sonar-db/src/main/java/org/sonar/db/component/ResourceKeyUpdaterDao.java index 4c0fb971c34..24fd831d68a 100644 --- a/sonar-db/src/main/java/org/sonar/db/component/ResourceKeyUpdaterDao.java +++ b/sonar-db/src/main/java/org/sonar/db/component/ResourceKeyUpdaterDao.java @@ -19,6 +19,7 @@ */ package org.sonar.db.component; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; @@ -26,18 +27,26 @@ import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; import org.apache.commons.lang.StringUtils; import org.apache.ibatis.session.SqlSession; +import org.sonar.api.resources.Qualifiers; import org.sonar.db.Dao; import org.sonar.db.DbSession; import org.sonar.db.MyBatis; +import static com.google.common.base.Preconditions.checkArgument; +import static org.sonar.core.component.ComponentKeys.isValidModuleKey; + /** * Class used to rename the key of a project and its resources. * * @since 3.2 */ public class ResourceKeyUpdaterDao implements Dao { + private static final Set PROJECT_OR_MODULE_QUALIFIERS = ImmutableSet.of(Qualifiers.PROJECT, Qualifiers.MODULE); + private MyBatis mybatis; public ResourceKeyUpdaterDao(MyBatis mybatis) { @@ -87,6 +96,29 @@ public class ResourceKeyUpdaterDao implements Dao { return result; } + public static void checkIsProjectOrModule(ComponentDto component) { + checkArgument(PROJECT_OR_MODULE_QUALIFIERS.contains(component.qualifier()), "Component updated must be a module or a key"); + } + + /** + * + * @return a map with currentKey/newKey is a bulk update was executed + */ + public Map simulateBulkUpdateKey(DbSession dbSession, String projectUuid, String stringToReplace, String replacementString) { + return collectAllModules(projectUuid, stringToReplace, mapper(dbSession)) + .stream() + .collect(Collectors.toMap( + ResourceDto::getKey, + component -> computeNewKey(component, stringToReplace, replacementString))); + } + + /** + * @return a map with the component key as key, and boolean as true if key already exists in db + */ + public Map checkComponentKeys(DbSession dbSession, List newComponentKeys) { + return newComponentKeys.stream().collect(Collectors.toMap(Function.identity(), key -> mapper(dbSession).countResourceByKey(key) > 0)); + } + public void bulkUpdateKey(DbSession session, String projectUuid, String stringToReplace, String replacementString) { ResourceKeyUpdaterMapper mapper = session.getMapper(ResourceKeyUpdaterMapper.class); // must SELECT first everything @@ -139,11 +171,15 @@ public class ResourceKeyUpdaterDao implements Dao { private static void checkNewNameOfAllModules(Set modules, String stringToReplace, String replacementString, ResourceKeyUpdaterMapper mapper) { for (ResourceDto module : modules) { - String newName = computeNewKey(module, stringToReplace, replacementString); - if (mapper.countResourceByKey(newName) > 0) { - throw new IllegalStateException("Impossible to update key: a resource with \"" + newName + "\" key already exists."); + String newKey = computeNewKey(module, stringToReplace, replacementString); + checkArgument(isValidModuleKey(newKey), "Malformed key for '%s'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.", newKey); + if (mapper.countResourceByKey(newKey) > 0) { + throw new IllegalArgumentException("Impossible to update key: a component with key \"" + newKey + "\" already exists."); } } } + private ResourceKeyUpdaterMapper mapper(DbSession dbSession) { + return dbSession.getMapper(ResourceKeyUpdaterMapper.class); + } } diff --git a/sonar-db/src/test/java/org/sonar/db/component/ResourceKeyUpdaterDaoTest.java b/sonar-db/src/test/java/org/sonar/db/component/ResourceKeyUpdaterDaoTest.java index df673897e46..fc6fe33e731 100644 --- a/sonar-db/src/test/java/org/sonar/db/component/ResourceKeyUpdaterDaoTest.java +++ b/sonar-db/src/test/java/org/sonar/db/component/ResourceKeyUpdaterDaoTest.java @@ -28,7 +28,9 @@ import org.sonar.api.utils.System2; import org.sonar.db.DbSession; import org.sonar.db.DbTester; +import static com.google.common.collect.Lists.newArrayList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; import static org.sonar.db.component.ComponentTesting.newFileDto; import static org.sonar.db.component.ComponentTesting.newProjectDto; @@ -87,8 +89,8 @@ public class ResourceKeyUpdaterDaoTest { public void shouldFailBulkUpdateKeyIfKeyAlreadyExist() { db.prepareDbUnit(getClass(), "shared.xml"); - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Impossible to update key: a resource with \"foo:struts-core\" key already exists."); + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Impossible to update key: a component with key \"foo:struts-core\" already exists."); underTest.bulkUpdateKey(dbSession, "A", "org.struts", "foo"); dbSession.commit(); @@ -116,6 +118,16 @@ public class ResourceKeyUpdaterDaoTest { underTest.updateKey(project.uuid(), newLongProjectKey); } + @Test + public void fail_when_new_key_is_invalid() { + ComponentDto project = componentDb.insertProject(); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Malformed key for 'my?project?key'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit."); + + underTest.bulkUpdateKey(dbSession, project.uuid(), project.key(), "my?project?key"); + } + @Test public void shouldCheckModuleKeysBeforeRenaming() { db.prepareDbUnit(getClass(), "shared.xml"); @@ -127,4 +139,25 @@ public class ResourceKeyUpdaterDaoTest { assertThat(checkResults.get("org.struts:struts-ui")).isEqualTo("foo:struts-ui"); } + @Test + public void check_component_keys() { + db.prepareDbUnit(getClass(), "shared.xml"); + + Map result = underTest.checkComponentKeys(dbSession, newArrayList("foo:struts", "foo:struts-core", "foo:struts-ui")); + + assertThat(result) + .hasSize(3) + .containsOnly(entry("foo:struts", false), entry("foo:struts-core", true), entry("foo:struts-ui", false)); + } + + @Test + public void simulate_bulk_update_key() { + db.prepareDbUnit(getClass(), "shared.xml"); + + Map result = underTest.simulateBulkUpdateKey(dbSession, "A", "org.struts", "foo"); + + assertThat(result) + .hasSize(3) + .containsOnly(entry("org.struts:struts", "foo:struts"), entry("org.struts:struts-core", "foo:struts-core"), entry("org.struts:struts-ui", "foo:struts-ui")); + } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/component/BulkUpdateWsRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/BulkUpdateWsRequest.java new file mode 100644 index 00000000000..881a8d0fe1a --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/BulkUpdateWsRequest.java @@ -0,0 +1,111 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonarqube.ws.client.component; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; + +public class BulkUpdateWsRequest { + private final String id; + private final String key; + private final String from; + private final String to; + private final boolean dryRun; + + public BulkUpdateWsRequest(Builder builder) { + this.id = builder.id; + this.key = builder.key; + this.from = builder.from; + this.to = builder.to; + this.dryRun = builder.dryRun; + } + + @CheckForNull + public String getId() { + return id; + } + + @CheckForNull + public String getKey() { + return key; + } + + public String getFrom() { + return from; + } + + public String getTo() { + return to; + } + + public boolean isDryRun() { + return dryRun; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String id; + private String key; + private String from; + private String to; + private boolean dryRun; + + private Builder() { + // enforce method constructor + } + + public Builder setId(@Nullable String id) { + this.id = id; + return this; + } + + public Builder setKey(@Nullable String key) { + this.key = key; + return this; + } + + public Builder setFrom(String from) { + this.from = from; + return this; + } + + public Builder setTo(String to) { + this.to = to; + return this; + } + + public Builder setDryRun(boolean dryRun) { + this.dryRun = dryRun; + return this; + } + + public BulkUpdateWsRequest build() { + checkArgument(from != null && !from.isEmpty(), "The string to match must not be empty"); + checkArgument(to != null && !to.isEmpty(), "The string replacement must not be empty"); + return new BulkUpdateWsRequest(this); + } + } +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java index ffcbe8715ed..b7e71d43a3e 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java @@ -20,6 +20,7 @@ package org.sonarqube.ws.client.component; import com.google.common.base.Joiner; +import org.sonarqube.ws.WsComponents.BulkUpdateKeyWsResponse; import org.sonarqube.ws.WsComponents.SearchWsResponse; import org.sonarqube.ws.WsComponents.ShowWsResponse; import org.sonarqube.ws.WsComponents.TreeWsResponse; @@ -32,11 +33,13 @@ import static org.sonarqube.ws.client.component.ComponentsWsParameters.ACTION_SH import static org.sonarqube.ws.client.component.ComponentsWsParameters.ACTION_TREE; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_BASE_COMPONENT_ID; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_BASE_COMPONENT_KEY; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_FROM; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_ID; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_KEY; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_NEW_KEY; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_QUALIFIERS; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_STRATEGY; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_TO; public class ComponentsService extends BaseService { @@ -81,4 +84,14 @@ public class ComponentsService extends BaseService { call(post); } + + public BulkUpdateKeyWsResponse bulkUpdateKey(BulkUpdateWsRequest request) { + PostRequest post = new PostRequest(path("bulk_update_key")) + .setParam(PARAM_ID, request.getId()) + .setParam(PARAM_KEY, request.getKey()) + .setParam(PARAM_FROM, request.getFrom()) + .setParam(PARAM_TO, request.getTo()); + + return call(post, BulkUpdateKeyWsResponse.parser()); + } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java index 456b46985a4..4ae95bf46b0 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java @@ -34,6 +34,9 @@ public class ComponentsWsParameters { public static final String PARAM_ID = "id"; public static final String PARAM_KEY = "key"; public static final String PARAM_NEW_KEY = "newKey"; + public static final String PARAM_FROM = "from"; + public static final String PARAM_TO = "to"; + public static final String PARAM_DRY_RUN = "dryRun"; private ComponentsWsParameters() { // static utility class diff --git a/sonar-ws/src/main/protobuf/ws-components.proto b/sonar-ws/src/main/protobuf/ws-components.proto index 128ade44c99..a14966e8acd 100644 --- a/sonar-ws/src/main/protobuf/ws-components.proto +++ b/sonar-ws/src/main/protobuf/ws-components.proto @@ -46,6 +46,17 @@ message ShowWsResponse { repeated Component ancestors = 3; } +// WS api/components/prepare_bulk_update_key +message BulkUpdateKeyWsResponse { + repeated Key keys = 1; + + message Key { + optional string key = 1; + optional string newKey = 2; + optional bool duplicate = 3; + } +} + message Component { optional string id = 1; optional string key = 2; -- 2.39.5