diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java index 9b3105b5ed..f506793d1a 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java @@ -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()); diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/CopyMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/update/CopyMigrationStrategy.java new file mode 100644 index 0000000000..6b05f02888 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/CopyMigrationStrategy.java @@ -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; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/InlineMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/update/InlineMigrationStrategy.java new file mode 100644 index 0000000000..1374461bea --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/InlineMigrationStrategy.java @@ -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; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/MigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/update/MigrationStrategy.java new file mode 100644 index 0000000000..6ff631208c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/MigrationStrategy.java @@ -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 implementationClass; + + MigrationStrategy(Class implementationClass) { + this.implementationClass = implementationClass; + } + + Instance from(Injector injector) { + return injector.getInstance(implementationClass); + } + + interface Instance { + Path migrate(String id, String name, String type); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/MigrationStrategyDao.java b/scm-webapp/src/main/java/sonia/scm/repository/update/MigrationStrategyDao.java new file mode 100644 index 0000000000..f98e697468 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/MigrationStrategyDao.java @@ -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 store; + + @Inject + public MigrationStrategyDao(ConfigurationStoreFactory storeFactory) { + store = storeFactory.withType(RepositoryMigrationPlan.class).withName("migration-plan").build(); + this.plan = store.getOptional().orElse(new RepositoryMigrationPlan()); + } + + public Optional get(String id) { + return plan.get(id); + } + + public void set(String repositoryId, MigrationStrategy strategy) { + plan.set(repositoryId, strategy); + store.set(plan); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java new file mode 100644 index 0000000000..c2ccb91713 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java @@ -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; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/RepositoryMigrationPlan.java b/scm-webapp/src/main/java/sonia/scm/repository/update/RepositoryMigrationPlan.java new file mode 100644 index 0000000000..126bca8a23 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/RepositoryMigrationPlan.java @@ -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 entries; + + RepositoryMigrationPlan() { + this(new RepositoryEntry[0]); + } + + RepositoryMigrationPlan(RepositoryEntry... entries) { + this.entries = new ArrayList<>(asList(entries)); + } + + Optional get(String repositoryId) { + return findEntry(repositoryId) + .map(RepositoryEntry::getDataMigrationStrategy); + } + + public void set(String repositoryId, MigrationStrategy strategy) { + Optional entry = findEntry(repositoryId); + if (entry.isPresent()) { + entry.get().setStrategy(strategy); + } else { + entries.add(new RepositoryEntry(repositoryId, strategy)); + } + } + + private Optional 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; + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java index 923a26fee5..7ce7fe463c 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java @@ -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) { diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyDaoTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyDaoTest.java new file mode 100644 index 0000000000..a7365ce656 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyDaoTest.java @@ -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 strategy = dao.get("any"); + + Assertions.assertThat(strategy).isEmpty(); + } + + @Test + void shouldReturnNewValue() throws JAXBException { + MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); + + dao.set("id", INLINE); + + Optional 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 strategy = dao.get("id"); + + Assertions.assertThat(strategy).contains(INLINE); + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyMock.java b/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyMock.java new file mode 100644 index 0000000000..79b79abb41 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyMock.java @@ -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 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; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java index 133f7c72ec..095155f5e8 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java @@ -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 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