Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2019-06-11 13:55:43 +02:00
110 changed files with 7002 additions and 4053 deletions

View File

@@ -0,0 +1,161 @@
package sonia.scm.update.group;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.group.Group;
import sonia.scm.group.xml.XmlGroupDAO;
import sonia.scm.migration.UpdateException;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.StoreConstants;
import sonia.scm.update.properties.V1Properties;
import sonia.scm.version.Version;
import javax.inject.Inject;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static sonia.scm.version.Version.parse;
@Extension
public class XmlGroupV1UpdateStep implements UpdateStep {
private static final Logger LOG = LoggerFactory.getLogger(XmlGroupV1UpdateStep.class);
private final SCMContextProvider contextProvider;
private final XmlGroupDAO groupDAO;
private final ConfigurationEntryStore<V1Properties> propertyStore;
@Inject
public XmlGroupV1UpdateStep(
SCMContextProvider contextProvider,
XmlGroupDAO groupDAO,
ConfigurationEntryStoreFactory configurationEntryStoreFactory
) {
this.contextProvider = contextProvider;
this.groupDAO = groupDAO;
this.propertyStore = configurationEntryStoreFactory
.withType(V1Properties.class)
.withName("group-properties-v1")
.build();
}
@Override
public void doUpdate() throws JAXBException {
Optional<Path> v1GroupsFile = determineV1File();
if (!v1GroupsFile.isPresent()) {
LOG.info("no v1 file for groups found");
return;
}
XmlGroupV1UpdateStep.V1GroupDatabase v1Database = readV1Database(v1GroupsFile.get());
if (v1Database.groupList != null && v1Database.groupList.groups != null) {
v1Database.groupList.groups.forEach(this::update);
}
}
@Override
public Version getTargetVersion() {
return parse("2.0.0");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.group.xml";
}
private void update(V1Group v1Group) {
LOG.debug("updating group {}", v1Group.name);
Group group = new Group(
v1Group.type,
v1Group.name,
v1Group.members);
group.setDescription(v1Group.description);
group.setCreationDate(v1Group.creationDate);
group.setLastModified(v1Group.lastModified);
groupDAO.add(group);
propertyStore.put(v1Group.name, v1Group.properties);
}
private XmlGroupV1UpdateStep.V1GroupDatabase readV1Database(Path v1GroupsFile) throws JAXBException {
JAXBContext jaxbContext = JAXBContext.newInstance(XmlGroupV1UpdateStep.V1GroupDatabase.class);
return (XmlGroupV1UpdateStep.V1GroupDatabase) jaxbContext.createUnmarshaller().unmarshal(v1GroupsFile.toFile());
}
private Optional<Path> determineV1File() {
Path existingGroupsFile = resolveConfigFile("groups");
Path groupsV1File = resolveConfigFile("groupsV1");
if (existingGroupsFile.toFile().exists()) {
try {
Files.move(existingGroupsFile, groupsV1File);
} catch (IOException e) {
throw new UpdateException("could not move old groups file to " + groupsV1File.toAbsolutePath());
}
LOG.info("moved old groups file to {}", groupsV1File.toAbsolutePath());
return of(groupsV1File);
}
return empty();
}
private Path resolveConfigFile(String name) {
return contextProvider
.resolve(
Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve(name + StoreConstants.FILE_EXTENSION)
);
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "group")
private static class V1Group {
private V1Properties properties;
private long creationDate;
private String description;
private Long lastModified;
private String name;
private String type;
@XmlElement(name = "members")
private List<String> members;
@Override
public String toString() {
return "V1Group{" +
"properties=" + properties +
", creationDate=" + creationDate + '\'' +
", description=" + description + '\'' +
", lastModified=" + lastModified + '\'' +
", name='" + name + '\'' +
", type='" + type + '\'' +
'}';
}
}
private static class GroupList {
@XmlElement(name = "group")
private List<V1Group> groups;
}
@XmlRootElement(name = "group-db")
@XmlAccessorType(XmlAccessType.FIELD)
private static class V1GroupDatabase {
private long creationTime;
private Long lastModified;
@XmlElement(name = "groups")
private XmlGroupV1UpdateStep.GroupList groupList;
}
}

View File

@@ -0,0 +1,14 @@
package sonia.scm.update.properties;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.List;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "properties")
public class V1Properties {
@XmlElement(name = "item")
private List<V1Property> properties;
}

View File

@@ -0,0 +1,10 @@
package sonia.scm.update.properties;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class V1Property {
private String key;
private String value;
}

View File

@@ -0,0 +1,60 @@
package sonia.scm.update.repository;
import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.stream.Stream;
abstract class BaseMigrationStrategy implements MigrationStrategy.Instance {
private final SCMContextProvider contextProvider;
BaseMigrationStrategy(SCMContextProvider contextProvider) {
this.contextProvider = contextProvider;
}
Path getSourceDataPath(String name, String type) {
return Arrays.stream(name.split("/"))
.reduce(getTypeDependentPath(type), (path, namePart) -> path.resolve(namePart), (p1, p2) -> p1);
}
Path getTypeDependentPath(String type) {
return contextProvider.getBaseDirectory().toPath().resolve("repositories").resolve(type);
}
Stream<Path> listSourceDirectory(Path sourceDirectory) {
try {
return Files.list(sourceDirectory);
} catch (IOException e) {
throw new UpdateException("could not read original directory", e);
}
}
void createDataDirectory(Path target) {
try {
Files.createDirectories(target);
} catch (IOException e) {
throw new UpdateException("could not create data directory " + target, e);
}
}
void moveFile(Path sourceFile, Path targetFile) {
try {
Files.move(sourceFile, targetFile);
} catch (IOException e) {
throw new UpdateException("could not move data file from " + sourceFile + " to " + targetFile, e);
}
}
void copyFile(Path sourceFile, Path targetFile) {
try {
Files.copy(sourceFile, targetFile);
} catch (IOException e) {
throw new UpdateException("could not copy original file from " + sourceFile + " to " + targetFile, e);
}
}
}

View File

@@ -0,0 +1,44 @@
package sonia.scm.update.repository;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryDirectoryHandler;
import sonia.scm.repository.RepositoryLocationResolver;
import javax.inject.Inject;
import java.nio.file.Files;
import java.nio.file.Path;
class CopyMigrationStrategy extends BaseMigrationStrategy {
private final RepositoryLocationResolver locationResolver;
@Inject
public CopyMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) {
super(contextProvider);
this.locationResolver = locationResolver;
}
@Override
public Path migrate(String id, String name, String type) {
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
Path targetDataPath = repositoryBasePath
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
Path sourceDataPath = getSourceDataPath(name, type);
copyData(sourceDataPath, targetDataPath);
return repositoryBasePath;
}
private void copyData(Path sourceDirectory, Path targetDirectory) {
createDataDirectory(targetDirectory);
listSourceDirectory(sourceDirectory).forEach(
sourceFile -> {
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
if (Files.isDirectory(sourceFile)) {
copyData(sourceFile, targetFile);
} else {
copyFile(sourceFile, targetFile);
}
}
);
}
}

View File

@@ -0,0 +1,41 @@
package sonia.scm.update.repository;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryDirectoryHandler;
import javax.inject.Inject;
import java.nio.file.Files;
import java.nio.file.Path;
class InlineMigrationStrategy extends BaseMigrationStrategy {
@Inject
public InlineMigrationStrategy(SCMContextProvider contextProvider) {
super(contextProvider);
}
@Override
public Path migrate(String id, String name, String type) {
Path repositoryBasePath = getSourceDataPath(name, type);
Path targetDataPath = repositoryBasePath
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
moveData(repositoryBasePath, targetDataPath);
return repositoryBasePath;
}
private void moveData(Path sourceDirectory, Path targetDirectory) {
createDataDirectory(targetDirectory);
listSourceDirectory(sourceDirectory)
.filter(sourceFile -> !targetDirectory.equals(sourceFile))
.forEach(
sourceFile -> {
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
if (Files.isDirectory(sourceFile)) {
moveData(sourceFile, targetFile);
} else {
moveFile(sourceFile, targetFile);
}
}
);
}
}

View File

@@ -0,0 +1,143 @@
package sonia.scm.update.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.migration.UpdateException;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.HealthCheckFailure;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryRole;
import sonia.scm.repository.xml.SingleRepositoryUpdateProcessor;
import sonia.scm.security.SystemRepositoryPermissionProvider;
import sonia.scm.version.Version;
import javax.inject.Inject;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import java.nio.file.Path;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@Extension
public class MigrateVerbsToPermissionRoles extends RepositoryUpdates.RepositoryUpdateType implements UpdateStep {
public static final Logger LOG = LoggerFactory.getLogger(MigrateVerbsToPermissionRoles.class);
private final SingleRepositoryUpdateProcessor updateProcessor;
private final SystemRepositoryPermissionProvider systemRepositoryPermissionProvider;
@Inject
public MigrateVerbsToPermissionRoles(SingleRepositoryUpdateProcessor updateProcessor, SystemRepositoryPermissionProvider systemRepositoryPermissionProvider) {
this.updateProcessor = updateProcessor;
this.systemRepositoryPermissionProvider = systemRepositoryPermissionProvider;
}
@Override
public void doUpdate() {
updateProcessor.doUpdate(this::update);
}
void update(String repositoryId, Path path) {
LOG.info("updating repository {}", repositoryId);
OldRepository oldRepository = readOldRepository(path);
Repository newRepository = createNewRepository(oldRepository);
writeNewRepository(path, newRepository);
}
private void writeNewRepository(Path path, Repository newRepository) {
try {
JAXBContext jaxbContext = JAXBContext.newInstance(Repository.class);
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(newRepository, path.resolve("metadata.xml").toFile());
} catch (JAXBException e) {
throw new UpdateException("could not read old repository structure", e);
}
}
private OldRepository readOldRepository(Path path) {
try {
JAXBContext jaxbContext = JAXBContext.newInstance(OldRepository.class);
return (OldRepository) jaxbContext.createUnmarshaller().unmarshal(path.resolve("metadata.xml").toFile());
} catch (JAXBException e) {
throw new UpdateException("could not read old repository structure", e);
}
}
private Repository createNewRepository(OldRepository oldRepository) {
Repository repository = new Repository(
oldRepository.id,
oldRepository.type,
oldRepository.namespace,
oldRepository.name,
oldRepository.contact,
oldRepository.description,
oldRepository.permissions.stream().map(this::updatePermission).toArray(RepositoryPermission[]::new)
);
repository.setCreationDate(oldRepository.creationDate);
repository.setHealthCheckFailures(oldRepository.healthCheckFailures);
repository.setLastModified(oldRepository.lastModified);
repository.setPublicReadable(oldRepository.publicReadable);
repository.setArchived(oldRepository.archived);
return repository;
}
private RepositoryPermission updatePermission(RepositoryPermission repositoryPermission) {
return findMatchingRole(repositoryPermission.getVerbs())
.map(roleName -> copyRepositoryPermissionWithRole(repositoryPermission, roleName))
.orElse(repositoryPermission);
}
private RepositoryPermission copyRepositoryPermissionWithRole(RepositoryPermission repositoryPermission, String roleName) {
return new RepositoryPermission(repositoryPermission.getName(), roleName, repositoryPermission.isGroupPermission());
}
private Optional<String> findMatchingRole(Collection<String> verbs) {
return systemRepositoryPermissionProvider.availableRoles()
.stream()
.filter(r -> roleMatchesVerbs(verbs, r))
.map(RepositoryRole::getName)
.findFirst();
}
private boolean roleMatchesVerbs(Collection<String> verbs, RepositoryRole r) {
return verbs.size() == r.getVerbs().size() && r.getVerbs().containsAll(verbs);
}
@Override
public Version getTargetVersion() {
return Version.parse("1");
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "repositories")
private static class OldRepository {
private String contact;
private Long creationDate;
private String description;
@XmlElement(name = "healthCheckFailure")
@XmlElementWrapper(name = "healthCheckFailures")
private List<HealthCheckFailure> healthCheckFailures;
private String id;
private Long lastModified;
private String namespace;
private String name;
@XmlElement(name = "permission")
private final Set<RepositoryPermission> permissions = new HashSet<>();
@XmlElement(name = "public")
private boolean publicReadable = false;
private boolean archived = false;
private String type;
}
}

View File

@@ -0,0 +1,26 @@
package sonia.scm.update.repository;
import com.google.inject.Injector;
import java.nio.file.Path;
enum MigrationStrategy {
COPY(CopyMigrationStrategy.class),
MOVE(MoveMigrationStrategy.class),
INLINE(InlineMigrationStrategy.class);
private Class<? extends Instance> implementationClass;
MigrationStrategy(Class<? extends Instance> implementationClass) {
this.implementationClass = implementationClass;
}
Instance from(Injector injector) {
return injector.getInstance(implementationClass);
}
interface Instance {
Path migrate(String id, String name, String type);
}
}

View File

@@ -0,0 +1,28 @@
package sonia.scm.update.repository;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import javax.inject.Inject;
import java.util.Optional;
public class MigrationStrategyDao {
private final RepositoryMigrationPlan plan;
private final ConfigurationStore<RepositoryMigrationPlan> store;
@Inject
public MigrationStrategyDao(ConfigurationStoreFactory storeFactory) {
store = storeFactory.withType(RepositoryMigrationPlan.class).withName("migration-plan").build();
this.plan = store.getOptional().orElse(new RepositoryMigrationPlan());
}
public Optional<MigrationStrategy> get(String id) {
return plan.get(id);
}
public void set(String repositoryId, MigrationStrategy strategy) {
plan.set(repositoryId, strategy);
store.set(plan);
}
}

View File

@@ -0,0 +1,75 @@
package sonia.scm.update.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryDirectoryHandler;
import sonia.scm.repository.RepositoryLocationResolver;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static java.util.Arrays.asList;
class MoveMigrationStrategy extends BaseMigrationStrategy {
private static final Logger LOG = LoggerFactory.getLogger(MoveMigrationStrategy.class);
private final RepositoryLocationResolver locationResolver;
@Inject
public MoveMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) {
super(contextProvider);
this.locationResolver = locationResolver;
}
@Override
public Path migrate(String id, String name, String type) {
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
Path targetDataPath = repositoryBasePath
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
Path sourceDataPath = getSourceDataPath(name, type);
moveData(sourceDataPath, targetDataPath);
deleteOldDataDir(getTypeDependentPath(type), name);
return repositoryBasePath;
}
private void deleteOldDataDir(Path rootPath, String name) {
delete(rootPath, asList(name.split("/")));
}
private void delete(Path rootPath, List<String> directories) {
if (directories.isEmpty()) {
return;
}
Path directory = rootPath.resolve(directories.get(0));
delete(directory, directories.subList(1, directories.size()));
try {
Files.deleteIfExists(directory);
} catch (IOException e) {
LOG.warn("could not delete source repository directory {}", directory);
}
}
private void moveData(Path sourceDirectory, Path targetDirectory) {
createDataDirectory(targetDirectory);
listSourceDirectory(sourceDirectory).forEach(
sourceFile -> {
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
if (Files.isDirectory(sourceFile)) {
moveData(sourceFile, targetFile);
} else {
moveFile(sourceFile, targetFile);
}
}
);
try {
Files.delete(sourceDirectory);
} catch (IOException e) {
LOG.warn("could not delete source repository directory {}", sourceDirectory);
}
}
}

View File

@@ -0,0 +1,69 @@
package sonia.scm.update.repository;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static java.util.Arrays.asList;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "repository-migration")
class RepositoryMigrationPlan {
private List<RepositoryEntry> entries;
RepositoryMigrationPlan() {
this(new RepositoryEntry[0]);
}
RepositoryMigrationPlan(RepositoryEntry... entries) {
this.entries = new ArrayList<>(asList(entries));
}
Optional<MigrationStrategy> get(String repositoryId) {
return findEntry(repositoryId)
.map(RepositoryEntry::getDataMigrationStrategy);
}
public void set(String repositoryId, MigrationStrategy strategy) {
Optional<RepositoryEntry> entry = findEntry(repositoryId);
if (entry.isPresent()) {
entry.get().setStrategy(strategy);
} else {
entries.add(new RepositoryEntry(repositoryId, strategy));
}
}
private Optional<RepositoryEntry> findEntry(String repositoryId) {
return entries.stream()
.filter(repositoryEntry -> repositoryId.equals(repositoryEntry.repositoryId))
.findFirst();
}
@XmlRootElement(name = "entries")
@XmlAccessorType(XmlAccessType.FIELD)
static class RepositoryEntry {
private String repositoryId;
private MigrationStrategy dataMigrationStrategy;
RepositoryEntry() {
}
RepositoryEntry(String repositoryId, MigrationStrategy dataMigrationStrategy) {
this.repositoryId = repositoryId;
this.dataMigrationStrategy = dataMigrationStrategy;
}
public MigrationStrategy getDataMigrationStrategy() {
return dataMigrationStrategy;
}
private void setStrategy(MigrationStrategy strategy) {
this.dataMigrationStrategy = strategy;
}
}
}

View File

@@ -0,0 +1,10 @@
package sonia.scm.update.repository;
public class RepositoryUpdates {
static class RepositoryUpdateType {
public String getAffectedDataType() {
return "repository";
}
}
}

View File

@@ -0,0 +1,60 @@
package sonia.scm.update.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.store.StoreConstants;
import sonia.scm.version.Version;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static sonia.scm.version.Version.parse;
/**
* Moves an existing <code>repositories.xml</code> file to <code>repository-paths.xml</code>.
* Note that this has to run <em>after</em> an old v1 repository database has been migrated to v2
* (see {@link XmlRepositoryV1UpdateStep}).
*/
@Extension
public class XmlRepositoryFileNameUpdateStep implements UpdateStep {
private static final Logger LOG = LoggerFactory.getLogger(XmlRepositoryFileNameUpdateStep.class);
private final SCMContextProvider contextProvider;
private final XmlRepositoryDAO repositoryDAO;
@Inject
public XmlRepositoryFileNameUpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO repositoryDAO) {
this.contextProvider = contextProvider;
this.repositoryDAO = repositoryDAO;
}
@Override
public void doUpdate() throws IOException {
Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
Path oldRepositoriesFile = configDir.resolve("repositories.xml");
Path newRepositoryPathsFile = configDir.resolve(PathBasedRepositoryLocationResolver.STORE_NAME + StoreConstants.FILE_EXTENSION);
if (Files.exists(oldRepositoriesFile)) {
LOG.info("moving old repositories database files to repository-paths file");
Files.move(oldRepositoriesFile, newRepositoryPathsFile);
repositoryDAO.refresh();
}
}
@Override
public Version getTargetVersion() {
return parse("2.0.1");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.repository.xml";
}
}

View File

@@ -0,0 +1,250 @@
package sonia.scm.update.repository;
import com.google.inject.Injector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateException;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.StoreConstants;
import sonia.scm.update.properties.V1Properties;
import sonia.scm.version.Version;
import javax.inject.Inject;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static sonia.scm.version.Version.parse;
/**
* Migrates SCM-Manager v1 repository data structure to SCM-Manager v2 data structure.
* That is:
* <ul>
* <li>The old <code>repositories.xml</code> file is read</li>
* <li>For each repository in this database,
* <ul>
* <li>a new entry in the new <code>repository-paths.xml</code> database is written,</li>
* <li>the data directory is moved or copied to a SCM v2 consistent directory. How this is done
* can be specified by a strategy (@see {@link MigrationStrategy}), that has to be set in
* a database file named <code>migration-plan.xml</code></li> (to create this file, use {@link MigrationStrategyDao}),
* and
* <li>the new <code>metadata.xml</code> file is created.</li>
* </ul>
* </li>
* </ul>
*/
@Extension
public class XmlRepositoryV1UpdateStep implements UpdateStep {
private static Logger LOG = LoggerFactory.getLogger(XmlRepositoryV1UpdateStep.class);
private final SCMContextProvider contextProvider;
private final XmlRepositoryDAO repositoryDao;
private final MigrationStrategyDao migrationStrategyDao;
private final Injector injector;
private final ConfigurationEntryStore<V1Properties> propertyStore;
@Inject
public XmlRepositoryV1UpdateStep(
SCMContextProvider contextProvider,
XmlRepositoryDAO repositoryDao,
MigrationStrategyDao migrationStrategyDao,
Injector injector,
ConfigurationEntryStoreFactory configurationEntryStoreFactory
) {
this.contextProvider = contextProvider;
this.repositoryDao = repositoryDao;
this.migrationStrategyDao = migrationStrategyDao;
this.injector = injector;
this.propertyStore = configurationEntryStoreFactory
.withType(V1Properties.class)
.withName("repository-properties-v1")
.build();
}
@Override
public Version getTargetVersion() {
return parse("2.0.0");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.repository.xml";
}
@Override
public void doUpdate() throws JAXBException {
if (!resolveV1File().exists()) {
LOG.info("no v1 repositories database file found");
return;
}
JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class);
readV1Database(jaxbContext).ifPresent(
v1Database -> {
v1Database.repositoryList.repositories.forEach(this::readMigrationStrategy);
v1Database.repositoryList.repositories.forEach(this::update);
backupOldRepositoriesFile();
}
);
}
private void backupOldRepositoriesFile() {
Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
Path oldRepositoriesFile = configDir.resolve("repositories.xml");
Path backupFile = configDir.resolve("repositories.xml.v1.backup");
LOG.info("moving old repositories database files to backup file {}", backupFile);
try {
Files.move(oldRepositoriesFile, backupFile);
} catch (IOException e) {
throw new UpdateException("could not backup old repository database file", e);
}
}
private void update(V1Repository v1Repository) {
Path destination = handleDataDirectory(v1Repository);
Repository repository = new Repository(
v1Repository.id,
v1Repository.type,
getNamespace(v1Repository),
getName(v1Repository),
v1Repository.contact,
v1Repository.description,
createPermissions(v1Repository));
LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.name, destination);
repositoryDao.add(repository, destination);
propertyStore.put(v1Repository.id, v1Repository.properties);
}
private Path handleDataDirectory(V1Repository v1Repository) {
MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository);
return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type);
}
private MigrationStrategy readMigrationStrategy(V1Repository v1Repository) {
return migrationStrategyDao.get(v1Repository.id)
.orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name));
}
private RepositoryPermission[] createPermissions(V1Repository v1Repository) {
if (v1Repository.permissions == null) {
return new RepositoryPermission[0];
}
return v1Repository.permissions
.stream()
.map(this::createPermission)
.toArray(RepositoryPermission[]::new);
}
private RepositoryPermission createPermission(V1Permission v1Permission) {
LOG.info("creating permission {} for {}", v1Permission.type, v1Permission.name);
return new RepositoryPermission(v1Permission.name, v1Permission.type, v1Permission.groupPermission);
}
private String getNamespace(V1Repository v1Repository) {
String[] nameParts = getNameParts(v1Repository.name);
return nameParts.length > 1 ? nameParts[0] : v1Repository.type;
}
private String getName(V1Repository v1Repository) {
String[] nameParts = getNameParts(v1Repository.name);
return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts);
}
private String concatPathElements(String[] nameParts) {
return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_"));
}
private String[] getNameParts(String v1Name) {
return v1Name.split("/");
}
private Optional<V1RepositoryDatabase> readV1Database(JAXBContext jaxbContext) throws JAXBException {
Object unmarshal = jaxbContext.createUnmarshaller().unmarshal(resolveV1File());
if (unmarshal instanceof V1RepositoryDatabase) {
return of((V1RepositoryDatabase) unmarshal);
} else {
return empty();
}
}
private File resolveV1File() {
return contextProvider
.resolve(
Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve("repositories" + StoreConstants.FILE_EXTENSION)
).toFile();
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "permissions")
private static class V1Permission {
private boolean groupPermission;
private String name;
private String type;
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "repositories")
private static class V1Repository {
private String contact;
private long creationDate;
private Long lastModified;
private String description;
private String id;
private String name;
private boolean isPublic;
private boolean archived;
private String type;
private List<V1Permission> permissions;
private V1Properties properties;
@Override
public String toString() {
return "V1Repository{" +
", contact='" + contact + '\'' +
", creationDate=" + creationDate +
", lastModified=" + lastModified +
", description='" + description + '\'' +
", id='" + id + '\'' +
", name='" + name + '\'' +
", isPublic=" + isPublic +
", archived=" + archived +
", type='" + type + '\'' +
'}';
}
}
private static class RepositoryList {
@XmlElement(name = "repository")
private List<V1Repository> repositories;
}
@XmlRootElement(name = "repository-db")
@XmlAccessorType(XmlAccessType.FIELD)
private static class V1RepositoryDatabase {
private long creationTime;
private Long lastModified;
@XmlElement(name = "repositories")
private RepositoryList repositoryList;
}
}

View File

@@ -0,0 +1,105 @@
package sonia.scm.update.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.security.AssignedPermission;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.StoreConstants;
import sonia.scm.version.Version;
import javax.inject.Inject;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.File;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.function.Consumer;
import static java.util.Optional.ofNullable;
import static sonia.scm.version.Version.parse;
@Extension
public class XmlSecurityV1UpdateStep implements UpdateStep {
private static final Logger LOG = LoggerFactory.getLogger(XmlSecurityV1UpdateStep.class);
private final SCMContextProvider contextProvider;
private final ConfigurationEntryStoreFactory configurationEntryStoreFactory;
@Inject
public XmlSecurityV1UpdateStep(SCMContextProvider contextProvider, ConfigurationEntryStoreFactory configurationEntryStoreFactory) {
this.contextProvider = contextProvider;
this.configurationEntryStoreFactory = configurationEntryStoreFactory;
}
@Override
public void doUpdate() throws JAXBException {
ConfigurationEntryStore<AssignedPermission> securityStore = createSecurityStore();
forAllAdmins(user -> createSecurityEntry(user, false, securityStore),
group -> createSecurityEntry(group, true, securityStore));
}
private void forAllAdmins(Consumer<String> userConsumer, Consumer<String> groupConsumer) throws JAXBException {
Path configDirectory = determineConfigDirectory();
Path existingConfigFile = configDirectory.resolve("config" + StoreConstants.FILE_EXTENSION);
if (existingConfigFile.toFile().exists()) {
forAllAdmins(existingConfigFile, userConsumer, groupConsumer);
}
}
private void forAllAdmins(
Path existingConfigFile, Consumer<String> userConsumer, Consumer<String> groupConsumer
) throws JAXBException {
JAXBContext jaxbContext = JAXBContext.newInstance(XmlSecurityV1UpdateStep.V1Configuration.class);
V1Configuration v1Configuration = (V1Configuration) jaxbContext.createUnmarshaller().unmarshal(existingConfigFile.toFile());
ofNullable(v1Configuration.adminUsers).ifPresent(users -> forAll(users, userConsumer));
ofNullable(v1Configuration.adminGroups).ifPresent(groups -> forAll(groups, groupConsumer));
}
private void forAll(String entries, Consumer<String> consumer) {
Arrays.stream(entries.split(",")).forEach(consumer);
}
@Override
public Version getTargetVersion() {
return parse("2.0.0");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.security.xml";
}
private void createSecurityEntry(String name, boolean group, ConfigurationEntryStore<AssignedPermission> securityStore) {
LOG.debug("setting admin permissions for {} {}", group? "group": "user", name);
securityStore.put(new AssignedPermission(name, group, "*"));
}
private ConfigurationEntryStore<AssignedPermission> createSecurityStore() {
return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build();
}
private Path determineConfigDirectory() {
return new File(contextProvider.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME).toPath();
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "scm-config")
private static class V1Configuration {
@XmlElement(name = "admin-users")
private String adminUsers;
@XmlElement(name = "admin-groups")
private String adminGroups;
}
}

View File

@@ -0,0 +1,175 @@
package sonia.scm.update.user;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateException;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.security.AssignedPermission;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.StoreConstants;
import sonia.scm.update.properties.V1Properties;
import sonia.scm.user.User;
import sonia.scm.user.xml.XmlUserDAO;
import sonia.scm.version.Version;
import javax.inject.Inject;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static sonia.scm.version.Version.parse;
@Extension
public class XmlUserV1UpdateStep implements UpdateStep {
private static final Logger LOG = LoggerFactory.getLogger(XmlUserV1UpdateStep.class);
private final SCMContextProvider contextProvider;
private final XmlUserDAO userDAO;
private final ConfigurationEntryStoreFactory configurationEntryStoreFactory;
private final ConfigurationEntryStore<V1Properties> propertyStore;
@Inject
public XmlUserV1UpdateStep(SCMContextProvider contextProvider, XmlUserDAO userDAO, ConfigurationEntryStoreFactory configurationEntryStoreFactory) {
this.contextProvider = contextProvider;
this.userDAO = userDAO;
this.configurationEntryStoreFactory = configurationEntryStoreFactory;
this.propertyStore = configurationEntryStoreFactory
.withType(V1Properties.class)
.withName("user-properties-v1")
.build();
}
@Override
public void doUpdate() throws JAXBException {
Optional<Path> v1UsersFile = determineV1File();
if (!v1UsersFile.isPresent()) {
LOG.info("no v1 file for users found");
return;
}
XmlUserV1UpdateStep.V1UserDatabase v1Database = readV1Database(v1UsersFile.get());
ConfigurationEntryStore<AssignedPermission> securityStore = createSecurityStore();
v1Database.userList.users.forEach(user -> update(user, securityStore));
}
@Override
public Version getTargetVersion() {
return parse("2.0.0");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.user.xml";
}
private void update(V1User v1User, ConfigurationEntryStore<AssignedPermission> securityStore) {
LOG.debug("updating user {}", v1User.name);
User user = new User(
v1User.name,
v1User.displayName,
v1User.mail,
v1User.password,
v1User.type,
v1User.active);
user.setCreationDate(v1User.creationDate);
user.setLastModified(v1User.lastModified);
userDAO.add(user);
if (v1User.admin) {
LOG.debug("setting admin permissions for user {}", v1User.name);
securityStore.put(new AssignedPermission(v1User.name, "*"));
}
propertyStore.put(v1User.name, v1User.properties);
}
private XmlUserV1UpdateStep.V1UserDatabase readV1Database(Path v1UsersFile) throws JAXBException {
JAXBContext jaxbContext = JAXBContext.newInstance(XmlUserV1UpdateStep.V1UserDatabase.class);
return (XmlUserV1UpdateStep.V1UserDatabase) jaxbContext.createUnmarshaller().unmarshal(v1UsersFile.toFile());
}
private ConfigurationEntryStore<AssignedPermission> createSecurityStore() {
return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build();
}
private Optional<Path> determineV1File() {
Path existingUsersFile = resolveConfigFile("users");
Path usersV1File = resolveConfigFile("usersV1");
if (existingUsersFile.toFile().exists()) {
try {
Files.move(existingUsersFile, usersV1File);
} catch (IOException e) {
throw new UpdateException("could not move old users file to " + usersV1File.toAbsolutePath());
}
LOG.info("moved old users file to {}", usersV1File.toAbsolutePath());
return of(usersV1File);
}
return empty();
}
private Path resolveConfigFile(String name) {
return contextProvider
.resolve(
Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve(name + StoreConstants.FILE_EXTENSION)
);
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "user")
private static class V1User {
private V1Properties properties;
private boolean admin;
private long creationDate;
private String displayName;
private Long lastModified;
private String mail;
private String name;
private String password;
private String type;
private boolean active;
@Override
public String toString() {
return "V1User{" +
"properties=" + properties +
", admin='" + admin + '\'' +
", creationDate=" + creationDate + '\'' +
", displayName=" + displayName + '\'' +
", lastModified=" + lastModified + '\'' +
", mail='" + mail + '\'' +
", name='" + name + '\'' +
", type='" + type + '\'' +
", active='" + active + '\'' +
'}';
}
}
private static class UserList {
@XmlElement(name = "user")
private List<V1User> users;
}
@XmlRootElement(name = "user-db")
@XmlAccessorType(XmlAccessType.FIELD)
private static class V1UserDatabase {
private long creationTime;
private Long lastModified;
@XmlElement(name = "users")
private XmlUserV1UpdateStep.UserList userList;
}
}