@@ -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); | |||
} | |||
} |
@@ -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<String> 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"); | |||
} | |||
} |
@@ -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.<br>" + | |||
"It's possible to simulate the bulk update by setting the parameter '%s' at true. No key is updated with a dry run.<br>" + | |||
"Ex: to rename a project with key 'my_project' to 'my_new_project' and all its sub-components keys, call the WS with parameters:" + | |||
"<ul>" + | |||
" <li>%s: my_project</li>" + | |||
" <li>%s: my_</li>" + | |||
" <li>%s: my_new_</li>" + | |||
"</ul>" + | |||
"Either '%s' or '%s' must be provided, not both.<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_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<String, String> newKeysByOldKeys = componentKeyUpdater.simulateBulkUpdateKey(dbSession, projectOrModule.uuid(), request.getFrom(), request.getTo()); | |||
Map<String, Boolean> 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<String, Boolean> 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<String, String> newKeysByOldKeys, Map<String, Boolean> 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(); | |||
} | |||
} |
@@ -34,6 +34,7 @@ public class ComponentsWsModule extends Module { | |||
TreeAction.class, | |||
ShowAction.class, | |||
SearchViewComponentsAction.class, | |||
UpdateKeyAction.class); | |||
UpdateKeyAction.class, | |||
BulkUpdateKeyAction.class); | |||
} | |||
} |
@@ -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 | |||
} | |||
] | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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"); |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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<String> 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<String, String> 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<String, Boolean> checkComponentKeys(DbSession dbSession, List<String> 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<ResourceDto> 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); | |||
} | |||
} |
@@ -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<String, Boolean> 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<String, String> 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")); | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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 |
@@ -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; |