/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.db.component;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
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.sonar.api.resources.Qualifiers;
import org.sonar.db.Dao;
import org.sonar.db.DbSession;
import static com.google.common.base.Preconditions.checkArgument;
import static org.sonar.core.component.ComponentKeys.checkModuleKey;
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 ComponentKeyUpdaterDao implements Dao {
private static final Set<String> PROJECT_OR_MODULE_QUALIFIERS = ImmutableSet.of(Qualifiers.PROJECT, Qualifiers.MODULE);
public void updateKey(DbSession dbSession, String projectUuid, String newKey) {
ComponentKeyUpdaterMapper mapper = dbSession.getMapper(ComponentKeyUpdaterMapper.class);
if (mapper.countResourceByKey(newKey) > 0) {
throw new IllegalArgumentException("Impossible to update key: a component with key \"" + newKey + "\" already exists.");
}
// must SELECT first everything
ResourceDto project = mapper.selectProject(projectUuid);
String projectOldKey = project.getKey();
List<ResourceDto> resources = mapper.selectProjectResources(projectUuid);
resources.add(project);
// and then proceed with the batch UPDATE at once
runBatchUpdateForAllResources(resources, projectOldKey, newKey, mapper);
dbSession.commit();
}
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 -> {
String newKey = computeNewKey(component.getKey(), stringToReplace, replacementString);
checkModuleKey(newKey);
return newKey;
}));
}
/**
* @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));
}
@VisibleForTesting
static String computeNewKey(String key, String stringToReplace, String replacementString) {
return key.replace(stringToReplace, replacementString);
}
public void bulkUpdateKey(DbSession session, String projectUuid, String stringToReplace, String replacementString) {
ComponentKeyUpdaterMapper mapper = session.getMapper(ComponentKeyUpdaterMapper.class);
// must SELECT first everything
Set<ResourceDto> modules = collectAllModules(projectUuid, stringToReplace, mapper);
checkNewNameOfAllModules(modules, stringToReplace, replacementString, mapper);
Map<ResourceDto, List<ResourceDto>> allResourcesByModuleMap = Maps.newHashMap();
for (ResourceDto module : modules) {
allResourcesByModuleMap.put(module, mapper.selectProjectResources(module.getUuid()));
}
// and then proceed with the batch UPDATE at once
for (ResourceDto module : modules) {
String oldModuleKey = module.getKey();
String newModuleKey = computeNewKey(module.getKey(), stringToReplace, replacementString);
Collection<ResourceDto> resources = Lists.newArrayList(module);
resources.addAll(allResourcesByModuleMap.get(module));
runBatchUpdateForAllResources(resources, oldModuleKey, newModuleKey, mapper);
}
}
private static void runBatchUpdateForAllResources(Collection<ResourceDto> resources, String oldKey, String newKey, ComponentKeyUpdaterMapper mapper) {
for (ResourceDto resource : resources) {
String oldResourceKey = resource.getKey();
String newResourceKey = newKey + oldResourceKey.substring(oldKey.length(), oldResourceKey.length());
resource.setKey(newResourceKey);
String oldResourceDeprecatedKey = resource.getDeprecatedKey();
if (StringUtils.isNotBlank(oldResourceDeprecatedKey)) {
String newResourceDeprecatedKey = newKey + oldResourceDeprecatedKey.substring(oldKey.length(), oldResourceDeprecatedKey.length());
resource.setDeprecatedKey(newResourceDeprecatedKey);
}
mapper.update(resource);
}
}
private static Set<ResourceDto> collectAllModules(String projectUuid, String stringToReplace, ComponentKeyUpdaterMapper mapper) {
ResourceDto project = mapper.selectProject(projectUuid);
Set<ResourceDto> modules = Sets.newHashSet();
if (project.getKey().contains(stringToReplace)) {
modules.add(project);
}
for (ResourceDto submodule : mapper.selectDescendantProjects(projectUuid)) {
modules.addAll(collectAllModules(submodule.getUuid(), stringToReplace, mapper));
}
return modules;
}
private static void checkNewNameOfAllModules(Set<ResourceDto> modules, String stringToReplace, String replacementString, ComponentKeyUpdaterMapper mapper) {
for (ResourceDto module : modules) {
String newKey = computeNewKey(module.getKey(), 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 static ComponentKeyUpdaterMapper mapper(DbSession dbSession) {
return dbSession.getMapper(ComponentKeyUpdaterMapper.class);
}
}