Browse Source

SONAR-7929 Create WS api/components/bulk_update_key

tags/6.1-RC1
Teryk Bellahsene 7 years ago
parent
commit
000f6b88a4

+ 21
- 1
it/it-tests/src/test/java/it/component/ComponentsWsTest.java View File

@@ -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);
}
}

+ 12
- 14
server/sonar-server/src/main/java/org/sonar/server/component/ComponentService.java View File

@@ -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");
}
}

+ 176
- 0
server/sonar-server/src/main/java/org/sonar/server/component/ws/BulkUpdateKeyAction.java View File

@@ -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();
}
}

+ 2
- 1
server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentsWsModule.java View File

@@ -34,6 +34,7 @@ public class ComponentsWsModule extends Module {
TreeAction.class,
ShowAction.class,
SearchViewComponentsAction.class,
UpdateKeyAction.class);
UpdateKeyAction.class,
BulkUpdateKeyAction.class);
}
}

+ 19
- 0
server/sonar-server/src/main/resources/org/sonar/server/component/ws/bulk_update_key-example.json View File

@@ -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
}
]
}

+ 21
- 68
server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceTest.java View File

@@ -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);
}

}

+ 1
- 1
server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java View File

@@ -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");

+ 293
- 0
server/sonar-server/src/test/java/org/sonar/server/component/ws/BulkUpdateKeyActionTest.java View File

@@ -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);
}
}
}

+ 1
- 1
server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsModuleTest.java View File

@@ -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);
}
}

+ 39
- 3
sonar-db/src/main/java/org/sonar/db/component/ResourceKeyUpdaterDao.java View File

@@ -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);
}
}

+ 35
- 2
sonar-db/src/test/java/org/sonar/db/component/ResourceKeyUpdaterDaoTest.java View File

@@ -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"));
}
}

+ 111
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/component/BulkUpdateWsRequest.java View File

@@ -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);
}
}
}

+ 13
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java View File

@@ -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());
}
}

+ 3
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java View File

@@ -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

+ 11
- 0
sonar-ws/src/main/protobuf/ws-components.proto View File

@@ -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;

Loading…
Cancel
Save