Bootstrap migration strategies

This commit is contained in:
René Pfeuffer
2019-05-22 14:28:53 +02:00
parent 3d28f800be
commit 1674bc2e7d
11 changed files with 335 additions and 10 deletions

View File

@@ -95,10 +95,17 @@ public class XmlRepositoryDAO implements RepositoryDAO {
@Override
public void add(Repository repository) {
add(repository, repositoryLocationResolver.create(repository.getId()));
}
public void add(Repository repository, Object location) {
if (!(location instanceof Path)) {
throw new IllegalArgumentException("can only handle locations of type " + Path.class.getName() + ", not of type " + location.getClass().getName());
}
Repository clone = repository.clone();
synchronized (this) {
Path repositoryPath = repositoryLocationResolver.create(repository.getId());
Path repositoryPath = (Path) location;
try {
Path metadataPath = resolveDataPath(repositoryPath);
@@ -111,10 +118,8 @@ public class XmlRepositoryDAO implements RepositoryDAO {
byId.put(repository.getId(), clone);
byNamespaceAndName.put(repository.getNamespaceAndName(), clone);
}
}
@Override
public boolean contains(Repository repository) {
return byId.containsKey(repository.getId());

View File

@@ -0,0 +1,21 @@
package sonia.scm.repository.update;
import sonia.scm.SCMContextProvider;
import javax.inject.Inject;
import java.nio.file.Path;
class CopyMigrationStrategy implements MigrationStrategy.Instance {
private final SCMContextProvider contextProvider;
@Inject
public CopyMigrationStrategy(SCMContextProvider contextProvider) {
this.contextProvider = contextProvider;
}
@Override
public Path migrate(String id, String name, String type) {
return null;
}
}

View File

@@ -0,0 +1,21 @@
package sonia.scm.repository.update;
import sonia.scm.SCMContextProvider;
import javax.inject.Inject;
import java.nio.file.Path;
class InlineMigrationStrategy implements MigrationStrategy.Instance {
private final SCMContextProvider contextProvider;
@Inject
public InlineMigrationStrategy(SCMContextProvider contextProvider) {
this.contextProvider = contextProvider;
}
@Override
public Path migrate(String id, String name, String type) {
return null;
}
}

View File

@@ -0,0 +1,26 @@
package sonia.scm.repository.update;
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.repository.update;
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,21 @@
package sonia.scm.repository.update;
import sonia.scm.SCMContextProvider;
import javax.inject.Inject;
import java.nio.file.Path;
class MoveMigrationStrategy implements MigrationStrategy.Instance {
private final SCMContextProvider contextProvider;
@Inject
public MoveMigrationStrategy(SCMContextProvider contextProvider) {
this.contextProvider = contextProvider;
}
@Override
public Path migrate(String id, String name, String type) {
return null;
}
}

View File

@@ -0,0 +1,69 @@
package sonia.scm.repository.update;
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

@@ -1,5 +1,8 @@
package sonia.scm.repository.update;
import com.google.inject.Injector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
@@ -17,6 +20,7 @@ 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.List;
import java.util.Map;
@@ -27,13 +31,19 @@ import static sonia.scm.version.Version.parse;
@Extension
public class XmlRepositoryV1UpdateStep implements UpdateStep {
private static Logger LOG = LoggerFactory.getLogger(XmlRepositoryV1UpdateStep.class);
private final SCMContextProvider contextProvider;
private final XmlRepositoryDAO dao;
private final XmlRepositoryDAO repositoryDao;
private final MigrationStrategyDao migrationStrategyDao;
private final Injector injector;
@Inject
public XmlRepositoryV1UpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO dao) {
public XmlRepositoryV1UpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO repositoryDao, MigrationStrategyDao migrationStrategyDao, Injector injector) {
this.contextProvider = contextProvider;
this.dao = dao;
this.repositoryDao = repositoryDao;
this.migrationStrategyDao = migrationStrategyDao;
this.injector = injector;
}
@Override
@@ -57,6 +67,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep {
}
private void update(V1Repository v1Repository) {
Path destination = handleDataDirectory(v1Repository);
Repository repository = new Repository(
v1Repository.id,
v1Repository.type,
@@ -65,7 +76,14 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep {
v1Repository.contact,
v1Repository.description,
createPermissions(v1Repository));
dao.add(repository);
repositoryDao.add(repository);
}
private Path handleDataDirectory(V1Repository v1Repository) {
MigrationStrategy dataMigrationStrategy =
migrationStrategyDao.get(v1Repository.id)
.orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name));
return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type);
}
private RepositoryPermission[] createPermissions(V1Repository v1Repository) {

View File

@@ -0,0 +1,75 @@
package sonia.scm.repository.update;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.JAXBConfigurationStoreFactory;
import javax.xml.bind.JAXBException;
import java.nio.file.Path;
import java.util.Optional;
import static org.mockito.Mockito.when;
import static sonia.scm.repository.update.MigrationStrategy.INLINE;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
class MigrationStrategyDaoTest {
@Mock
SCMContextProvider contextProvider;
private ConfigurationStoreFactory storeFactory;
@BeforeEach
void initStore(@TempDirectory.TempDir Path tempDir) {
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
storeFactory = new JAXBConfigurationStoreFactory(contextProvider, null);
}
@Test
void shouldReturnEmptyOptionalWhenStoreIsEmpty() throws JAXBException {
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
Optional<MigrationStrategy> strategy = dao.get("any");
Assertions.assertThat(strategy).isEmpty();
}
@Test
void shouldReturnNewValue() throws JAXBException {
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
dao.set("id", INLINE);
Optional<MigrationStrategy> strategy = dao.get("id");
Assertions.assertThat(strategy).contains(INLINE);
}
@Nested
class WithExistingDatabase {
@BeforeEach
void initExistingDatabase() throws JAXBException {
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
dao.set("id", INLINE);
}
@Test
void shouldFindExistingValue() throws JAXBException {
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
Optional<MigrationStrategy> strategy = dao.get("id");
Assertions.assertThat(strategy).contains(INLINE);
}
}
}

View File

@@ -0,0 +1,24 @@
package sonia.scm.repository.update;
import com.google.inject.Injector;
import java.util.HashMap;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class MigrationStrategyMock {
static Injector init() {
Map<Class, MigrationStrategy.Instance> mocks = new HashMap<>();
Injector mock = mock(Injector.class);
when(
mock.getInstance(any(Class.class)))
.thenAnswer(
invocationOnMock -> mocks.getOrDefault(invocationOnMock.getArgument(0), mock(invocationOnMock.getArgument(0)))
);
return mock;
}
}

View File

@@ -1,5 +1,6 @@
package sonia.scm.repository.update;
import com.google.inject.Injector;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -21,21 +22,30 @@ import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import static java.util.Optional.of;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static sonia.scm.repository.update.MigrationStrategy.COPY;
import static sonia.scm.repository.update.MigrationStrategy.INLINE;
import static sonia.scm.repository.update.MigrationStrategy.MOVE;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
class XmlRepositoryV1UpdateStepTest {
Injector injectorMock = MigrationStrategyMock.init();
@Mock
SCMContextProvider contextProvider;
@Mock
XmlRepositoryDAO dao;
XmlRepositoryDAO repositoryDAO;
@Mock()
MigrationStrategyDao migrationStrategyDao;
@Captor
ArgumentCaptor<Repository> storeCaptor;
@@ -58,13 +68,20 @@ class XmlRepositoryV1UpdateStepTest {
@BeforeEach
void captureStoredRepositories() {
doNothing().when(dao).add(storeCaptor.capture());
doNothing().when(repositoryDAO).add(storeCaptor.capture());
}
@BeforeEach
void createMigrationPlan(@TempDirectory.TempDir Path tempDir) {
lenient().when(migrationStrategyDao.get("3b91caa5-59c3-448f-920b-769aaa56b761")).thenReturn(of(MOVE));
lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(of(COPY));
lenient().when(migrationStrategyDao.get("454972da-faf9-4437-b682-dc4a4e0aa8eb")).thenReturn(of(INLINE));
}
@Test
void shouldCreateNewRepositories() throws JAXBException {
updateStep.doUpdate();
verify(dao, times(3)).add(any());
verify(repositoryDAO, times(3)).add(any());
}
@Test