From 93025629e65da63ad444d2a601d443055081a481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 15 May 2019 15:57:18 +0200 Subject: [PATCH 01/57] Migrate verbs to roles if possible --- .../xml/SingleRepositoryUpdateProcessor.java | 15 ++ .../update/MigrateVerbsToPermissionRoles.java | 143 ++++++++++++++++++ .../repository/update/RepositoryUpdates.java | 10 ++ .../SystemRepositoryPermissionProvider.java | 2 +- .../MigrateVerbsToPermissionRolesTest.java | 76 ++++++++++ .../update/metadataWithoutRoles.xml | 25 +++ 6 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/update/MigrateVerbsToPermissionRoles.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/update/RepositoryUpdates.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/update/MigrateVerbsToPermissionRolesTest.java create mode 100644 scm-webapp/src/test/resources/sonia/scm/repository/update/metadataWithoutRoles.xml diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java new file mode 100644 index 0000000000..eeb95f75b0 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java @@ -0,0 +1,15 @@ +package sonia.scm.repository.xml; + +import javax.inject.Inject; +import java.nio.file.Path; +import java.util.function.BiConsumer; + +public class SingleRepositoryUpdateProcessor { + + @Inject + private PathBasedRepositoryLocationResolver locationResolver; + + public void doUpdate(BiConsumer forEachRepository) { + locationResolver.forAllPaths(forEachRepository); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/MigrateVerbsToPermissionRoles.java b/scm-webapp/src/main/java/sonia/scm/repository/update/MigrateVerbsToPermissionRoles.java new file mode 100644 index 0000000000..122b9a25e8 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/MigrateVerbsToPermissionRoles.java @@ -0,0 +1,143 @@ +package sonia.scm.repository.update; + +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 findMatchingRole(Collection verbs) { + return systemRepositoryPermissionProvider.availableRoles() + .stream() + .filter(r -> roleMatchesVerbs(verbs, r)) + .map(RepositoryRole::getName) + .findFirst(); + } + + private boolean roleMatchesVerbs(Collection 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 healthCheckFailures; + private String id; + private Long lastModified; + private String namespace; + private String name; + @XmlElement(name = "permission") + private final Set permissions = new HashSet<>(); + @XmlElement(name = "public") + private boolean publicReadable = false; + private boolean archived = false; + private String type; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/RepositoryUpdates.java b/scm-webapp/src/main/java/sonia/scm/repository/update/RepositoryUpdates.java new file mode 100644 index 0000000000..2c814605c4 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/RepositoryUpdates.java @@ -0,0 +1,10 @@ +package sonia.scm.repository.update; + +public class RepositoryUpdates { + + static class RepositoryUpdateType { + public String getAffectedDataType() { + return "repository"; + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java b/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java index 0350698352..444ee3e941 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/security/SystemRepositoryPermissionProvider.java @@ -25,7 +25,7 @@ import java.util.Set; import static java.util.Collections.unmodifiableCollection; import static java.util.stream.Collectors.toList; -class SystemRepositoryPermissionProvider { +public class SystemRepositoryPermissionProvider { private static final Logger logger = LoggerFactory.getLogger(SystemRepositoryPermissionProvider.class); private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml"; diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/MigrateVerbsToPermissionRolesTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/MigrateVerbsToPermissionRolesTest.java new file mode 100644 index 0000000000..8480bd76d0 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/MigrateVerbsToPermissionRolesTest.java @@ -0,0 +1,76 @@ +package sonia.scm.repository.update; + +import com.google.common.io.Resources; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.xml.SingleRepositoryUpdateProcessor; +import sonia.scm.security.SystemRepositoryPermissionProvider; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; + +import static java.util.Arrays.asList; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) +class MigrateVerbsToPermissionRolesTest { + + private static final String EXISTING_REPOSITORY_ID = "id"; + + @Mock + private SingleRepositoryUpdateProcessor singleRepositoryUpdateProcessor; + @Mock + private SystemRepositoryPermissionProvider systemRepositoryPermissionProvider; + + @InjectMocks + private MigrateVerbsToPermissionRoles migration; + + @BeforeEach + void init(@TempDirectory.TempDir Path tempDir) throws IOException { + URL metadataUrl = Resources.getResource("sonia/scm/repository/update/metadataWithoutRoles.xml"); + Files.copy(metadataUrl.openStream(), tempDir.resolve("metadata.xml")); + doAnswer(invocation -> { + ((BiConsumer) invocation.getArgument(0)).accept(EXISTING_REPOSITORY_ID, tempDir); + return null; + }).when(singleRepositoryUpdateProcessor).doUpdate(any()); + when(systemRepositoryPermissionProvider.availableRoles()).thenReturn(Collections.singletonList(new RepositoryRole("ROLE", asList("read", "write"), ""))); + } + + @Test + void x(@TempDirectory.TempDir Path tempDir) throws IOException { + migration.doUpdate(); + + List newMetadata = Files.readAllLines(tempDir.resolve("metadata.xml")); + Assertions.assertThat(newMetadata.stream().map(String::trim)). + containsSubsequence( + "false", + "user", + "ROLE" + ) + .containsSubsequence( + "true", + "group", + "special" + ) + .doesNotContain( + "read", + "write" + ); + } + +} diff --git a/scm-webapp/src/test/resources/sonia/scm/repository/update/metadataWithoutRoles.xml b/scm-webapp/src/test/resources/sonia/scm/repository/update/metadataWithoutRoles.xml new file mode 100644 index 0000000000..4716943f47 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/repository/update/metadataWithoutRoles.xml @@ -0,0 +1,25 @@ + + + + ich@du.er + 1557729536519 + + B3RQKYNzo2 + 1557825677782 + scmadmin + git + + false + user + read + write + + + true + group + special + + false + false + git + From b253cd110dc785543c629c39e5732ec1e4951e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 4 Jun 2019 16:37:53 +0200 Subject: [PATCH 02/57] Initial migration servlet --- .../scm/security/DefaultCipherHandler.java | 17 +++-- .../scm/boot/BootstrapContextListener.java | 48 +++++++++--- .../MigrationWizardContextListener.java | 22 ++++++ .../scm/update/MigrationWizardModule.java | 14 ++++ .../scm/update/MigrationWizardServlet.java | 75 +++++++++++++++++++ .../update/repository/MigrationStrategy.java | 2 +- .../repository/MigrationStrategyDao.java | 2 + .../repository/XmlRepositoryV1UpdateStep.java | 75 +++++++++++++------ .../repository-migration-restart.mustache | 10 +++ .../templates/repository-migration.mustache | 38 ++++++++++ 10 files changed, 264 insertions(+), 39 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java create mode 100644 scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java create mode 100644 scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java create mode 100644 scm-webapp/src/main/resources/templates/repository-migration-restart.mustache create mode 100644 scm-webapp/src/main/resources/templates/repository-migration.mustache diff --git a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java index 9c1fa590cc..8c3afbd90f 100644 --- a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java +++ b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java @@ -161,10 +161,17 @@ public class DefaultCipherHandler implements CipherHandler { * @return decrypted value */ public String decode(char[] plainKey, String value) { - String result = null; - + Base64.Decoder decoder = Base64.getUrlDecoder(); try { - byte[] encodedInput = Base64.getUrlDecoder().decode(value); + return decode(plainKey, value, decoder); + } catch (IllegalArgumentException e) { + return decode(plainKey, value, Base64.getDecoder()); + } + } + + private String decode(char[] plainKey, String value, Base64.Decoder decoder) { + try { + byte[] encodedInput = decoder.decode(value); byte[] salt = new byte[SALT_LENGTH]; byte[] encoded = new byte[encodedInput.length - SALT_LENGTH]; @@ -180,12 +187,10 @@ public class DefaultCipherHandler implements CipherHandler { byte[] decoded = cipher.doFinal(encoded); - result = new String(decoded, ENCODING); + return new String(decoded, ENCODING); } catch (IOException | GeneralSecurityException ex) { throw new CipherException("could not decode string", ex); } - - return result; } @Override diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index 3af8a76650..be7955ea28 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -46,6 +46,7 @@ import sonia.scm.ScmEventBusModule; import sonia.scm.ScmInitializerModule; import sonia.scm.Stage; import sonia.scm.event.ScmEventBus; +import sonia.scm.migration.UpdateException; import sonia.scm.plugin.DefaultPluginLoader; import sonia.scm.plugin.Plugin; import sonia.scm.plugin.PluginException; @@ -54,6 +55,7 @@ import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginWrapper; import sonia.scm.plugin.PluginsInternal; import sonia.scm.plugin.SmpArchive; +import sonia.scm.update.MigrationWizardContextListener; import sonia.scm.update.UpdateEngine; import sonia.scm.util.ClassLoaders; import sonia.scm.util.IOUtil; @@ -110,14 +112,16 @@ public class BootstrapContextListener implements ServletContextListener { public void contextDestroyed(ServletContextEvent sce) { contextListener.contextDestroyed(sce); - for (PluginWrapper plugin : contextListener.getPlugins()) { - ClassLoader pcl = plugin.getClassLoader(); + if (contextListener instanceof ScmContextListener) { + for (PluginWrapper plugin : ((ScmContextListener) contextListener).getPlugins()) { + ClassLoader pcl = plugin.getClassLoader(); - if (pcl instanceof Closeable) { - try { - ((Closeable) pcl).close(); - } catch (IOException ex) { - logger.warn("could not close plugin classloader", ex); + if (pcl instanceof Closeable) { + try { + ((Closeable) pcl).close(); + } catch (IOException ex) { + logger.warn("could not close plugin classloader", ex); + } } } } @@ -151,7 +155,9 @@ public class BootstrapContextListener implements ServletContextListener { } private void createContextListener(File pluginDirectory) { + try { + renameOldPluginsFolder(pluginDirectory); if (!isCorePluginExtractionDisabled()) { extractCorePlugins(context, pluginDirectory); } else { @@ -166,14 +172,36 @@ public class BootstrapContextListener implements ServletContextListener { Injector bootstrapInjector = createBootstrapInjector(pluginLoader); - processUpdates(pluginLoader, bootstrapInjector); + MigrationWizardContextListener wizardContextListener = prepareWizardIfNeeded(bootstrapInjector); - contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins); + if (wizardContextListener.wizardNecessary()) { + contextListener = wizardContextListener; + } else { + processUpdates(pluginLoader, bootstrapInjector); + + contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins); + } } catch (IOException ex) { throw new PluginLoadException("could not load plugins", ex); } } + private void renameOldPluginsFolder(File pluginDirectory) { + if (new File(pluginDirectory, "classpath.xml").exists()) { + File backupDirectory = new File(pluginDirectory.getParentFile(), "plugins.v1"); + boolean renamed = pluginDirectory.renameTo(backupDirectory); + if (renamed) { + logger.warn("moved old plugins directory to {}", backupDirectory); + } else { + throw new UpdateException("could not rename existing v1 plugin directory"); + } + } + } + + private MigrationWizardContextListener prepareWizardIfNeeded(Injector bootstrapInjector) { + return new MigrationWizardContextListener(bootstrapInjector); + } + private Injector createBootstrapInjector(PluginLoader pluginLoader) { Module scmContextListenerModule = new ScmContextListenerModule(); BootstrapModule bootstrapModule = new BootstrapModule(pluginLoader); @@ -402,7 +430,7 @@ public class BootstrapContextListener implements ServletContextListener { private ServletContext context; /** Field description */ - private ScmContextListener contextListener; + private ServletContextListener contextListener; /** Field description */ private boolean registered = false; diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java new file mode 100644 index 0000000000..c98062c900 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java @@ -0,0 +1,22 @@ +package sonia.scm.update; + +import com.google.inject.Injector; +import com.google.inject.servlet.GuiceServletContextListener; + +public class MigrationWizardContextListener extends GuiceServletContextListener { + + private final Injector injector; + + public MigrationWizardContextListener(Injector bootstrapInjector) { + this.injector = bootstrapInjector.createChildInjector(new MigrationWizardModule()); + } + + public boolean wizardNecessary() { + return injector.getInstance(MigrationWizardServlet.class).wizardNecessary(); + } + + @Override + protected Injector getInjector() { + return injector; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java new file mode 100644 index 0000000000..3362496330 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java @@ -0,0 +1,14 @@ +package sonia.scm.update; + +import com.google.inject.servlet.ServletModule; +import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; + +import java.util.List; + +class MigrationWizardModule extends ServletModule { + + @Override + protected void configureServlets() { + serve("/*").with(MigrationWizardServlet.class); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java new file mode 100644 index 0000000000..d65a8408a0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -0,0 +1,75 @@ +package sonia.scm.update; + +import com.github.mustachejava.DefaultMustacheFactory; +import com.github.mustachejava.Mustache; +import com.github.mustachejava.MustacheFactory; +import sonia.scm.boot.RestartEvent; +import sonia.scm.event.ScmEventBus; +import sonia.scm.update.repository.MigrationStrategy; +import sonia.scm.update.repository.MigrationStrategyDao; +import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; + +@Singleton +class MigrationWizardServlet extends HttpServlet { + + private final XmlRepositoryV1UpdateStep repositoryV1UpdateStep; + private final MigrationStrategyDao migrationStrategyDao; + + @Inject + MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, MigrationStrategyDao migrationStrategyDao) { + this.repositoryV1UpdateStep = repositoryV1UpdateStep; + this.migrationStrategyDao = migrationStrategyDao; + } + + public boolean wizardNecessary() { + return !repositoryV1UpdateStep.missingMigrationStrategies().isEmpty(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + List missingMigrationStrategies = repositoryV1UpdateStep.missingMigrationStrategies(); + + resp.setStatus(200); + + HashMap model = new HashMap<>(); + + model.put("submitUrl", req.getRequestURI()); + model.put("repositories", missingMigrationStrategies); + model.put("strategies", getMigrationStrategies()); + + MustacheFactory mf = new DefaultMustacheFactory(); + Mustache mustache = mf.compile("templates/repository-migration.mustache"); + mustache.execute(resp.getWriter(), model).flush(); + } + + private List getMigrationStrategies() { + return stream(MigrationStrategy.values()).map(Enum::name).collect(toList()); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setStatus(200); + + req.getParameterMap().forEach( + (name, strategy) -> migrationStrategyDao.set(name, MigrationStrategy.valueOf(strategy[0])) + ); + + MustacheFactory mf = new DefaultMustacheFactory(); + Mustache mustache = mf.compile("templates/repository-migration-restart.mustache"); + mustache.execute(resp.getWriter(), new Object()).flush(); + + ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data")); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java index c7bb2cba86..d4b6c5c0f5 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java @@ -4,7 +4,7 @@ import com.google.inject.Injector; import java.nio.file.Path; -enum MigrationStrategy { +public enum MigrationStrategy { COPY(CopyMigrationStrategy.class), MOVE(MoveMigrationStrategy.class), diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java index 15c931bf31..8ddb5e02df 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java @@ -4,8 +4,10 @@ import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStoreFactory; import javax.inject.Inject; +import javax.inject.Singleton; import java.util.Optional; +@Singleton public class MigrationStrategyDao { private final RepositoryMigrationPlan plan; diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index 34c08fb16b..acb28ca432 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -32,7 +32,9 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; +import static java.util.Collections.emptyList; import static java.util.Optional.empty; import static java.util.Optional.of; import static sonia.scm.version.Version.parse; @@ -109,6 +111,23 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { ); } + public List missingMigrationStrategies() { + if (!resolveV1File().exists()) { + LOG.info("no v1 repositories database file found"); + return emptyList(); + } + try { + JAXBContext jaxbContext = JAXBContext.newInstance(XmlRepositoryV1UpdateStep.V1RepositoryDatabase.class); + return readV1Database(jaxbContext) + .map(v1Database -> v1Database.repositoryList.repositories.stream()) + .orElse(Stream.empty()) + .filter(v1Repository -> !this.findMigrationStrategy(v1Repository).isPresent()) + .collect(Collectors.toList()); + } catch (JAXBException e) { + throw new UpdateException("could not read v1 repository database", e); + } + } + private void backupOldRepositoriesFile() { Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME); Path oldRepositoriesFile = configDir.resolve("repositories.xml"); @@ -126,8 +145,8 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { Repository repository = new Repository( v1Repository.id, v1Repository.type, - getNamespace(v1Repository), - getName(v1Repository), + v1Repository.getNewNamespace(), + v1Repository.getNewName(), v1Repository.contact, v1Repository.description, createPermissions(v1Repository)); @@ -142,10 +161,14 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { } private MigrationStrategy readMigrationStrategy(V1Repository v1Repository) { - return migrationStrategyDao.get(v1Repository.id) + return findMigrationStrategy(v1Repository) .orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name)); } + private Optional findMigrationStrategy(V1Repository v1Repository) { + return migrationStrategyDao.get(v1Repository.id); + } + private RepositoryPermission[] createPermissions(V1Repository v1Repository) { if (v1Repository.permissions == null) { return new RepositoryPermission[0]; @@ -161,24 +184,6 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { 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 readV1Database(JAXBContext jaxbContext) throws JAXBException { Object unmarshal = jaxbContext.createUnmarshaller().unmarshal(resolveV1File()); if (unmarshal instanceof V1RepositoryDatabase) { @@ -205,7 +210,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "repositories") - private static class V1Repository { + public static class V1Repository { private String contact; private long creationDate; private Long lastModified; @@ -218,6 +223,32 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { private List permissions; private V1Properties properties; + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getNewNamespace() { + String[] nameParts = getNameParts(name); + return nameParts.length > 1 ? nameParts[0] : type; + } + + public String getNewName() { + String[] nameParts = getNameParts(name); + return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts); + } + + private String[] getNameParts(String v1Name) { + return v1Name.split("/"); + } + + private String concatPathElements(String[] nameParts) { + return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_")); + } + @Override public String toString() { return "V1Repository{" + diff --git a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache new file mode 100644 index 0000000000..e47945174a --- /dev/null +++ b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache @@ -0,0 +1,10 @@ + + + + SCM-Manager Restart + + + +SCM-Manager will restart to migrate the data. + + diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache new file mode 100644 index 0000000000..641e718136 --- /dev/null +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -0,0 +1,38 @@ + + + + SCM-Manager Migration + + + +

SCM-Manager Migration

+You have migrated from SCM-Manager v1 to SCM-Manager v2. +
+ + + + + + + {{#repositories}} + + + + + + {{/repositories}} +
original namenew namespace/nameStrategy
+ {{name}} + + {{newNamespace}}/{{newName}} + + +
+ +
+ + From e52518a12b8eecb581f2ce3f2bf6430076ffb6b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 5 Jun 2019 08:21:37 +0200 Subject: [PATCH 03/57] Cleanup - Mark PathBasedRepositoryLocationResolver as singleton so that other users will get the same instance and will not overwrite the paths set by migration. - Set path kept by InlineMigrationStrategy in location resolver to store the path. - Add logging - Add type of repository to migration web page --- .../RepositoryLocationResolver.java | 3 +- .../PathBasedRepositoryLocationResolver.java | 28 ++++++++++++++----- .../repository/xml/XmlRepositoryDAOTest.java | 14 +++++++++- .../TempDirRepositoryLocationResolver.java | 13 +++++++-- .../scm/update/MigrationWizardModule.java | 12 ++++++-- .../repository/CopyMigrationStrategy.java | 5 ++++ .../repository/InlineMigrationStrategy.java | 12 +++++++- .../repository/MoveMigrationStrategy.java | 1 + .../repository/XmlRepositoryV1UpdateStep.java | 5 ++++ .../templates/repository-migration.mustache | 4 +++ .../InlineMigrationStrategyTest.java | 13 +++++++-- 11 files changed, 93 insertions(+), 17 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java index bc8df673d0..8c76bdabaa 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java @@ -13,8 +13,9 @@ public abstract class RepositoryLocationResolver { return create(type); } - @FunctionalInterface public interface RepositoryLocationResolverInstance { T getLocation(String repositoryId); + + void setLocation(String repositoryId, T location); } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java index a1ce516069..dccdbc197b 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -7,6 +7,7 @@ import sonia.scm.repository.InternalRepositoryException; import sonia.scm.store.StoreConstants; import javax.inject.Inject; +import javax.inject.Singleton; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -28,6 +29,7 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity; * * @since 2.0.0 */ +@Singleton public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver { public static final String STORE_NAME = "repository-paths"; @@ -64,19 +66,26 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation @Override protected RepositoryLocationResolverInstance create(Class type) { - return repositoryId -> { - if (pathById.containsKey(repositoryId)) { - return (T) contextProvider.resolve(pathById.get(repositoryId)); - } else { - return (T) create(repositoryId); + return new RepositoryLocationResolverInstance() { + @Override + public T getLocation(String repositoryId) { + if (pathById.containsKey(repositoryId)) { + return (T) contextProvider.resolve(pathById.get(repositoryId)); + } else { + return (T) create(repositoryId); + } + } + + @Override + public void setLocation(String repositoryId, T location) { + PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, (Path) location); } }; } Path create(String repositoryId) { Path path = initialRepositoryLocationResolver.getPath(repositoryId); - pathById.put(repositoryId, path); - writePathDatabase(); + setLocation(repositoryId, path); Path resolvedPath = contextProvider.resolve(path); try { Files.createDirectories(resolvedPath); @@ -138,4 +147,9 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation .resolve(StoreConstants.CONFIG_DIRECTORY_NAME) .resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION)); } + + public void setLocation(String repositoryId, Path repositoryBasePath) { + pathById.put(repositoryId, repositoryBasePath); + writePathDatabase(); + } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java index 5b9a00aec8..36c9db33e2 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java @@ -19,6 +19,7 @@ import sonia.scm.io.DefaultFileSystem; import sonia.scm.io.FileSystem; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.repository.RepositoryPermission; import java.io.IOException; @@ -56,7 +57,18 @@ class XmlRepositoryDAOTest { @BeforeEach void createDAO(@TempDirectory.TempDir Path basePath) { - when(locationResolver.create(Path.class)).thenReturn(locationResolver::create); + when(locationResolver.create(Path.class)).thenReturn( + new RepositoryLocationResolver.RepositoryLocationResolverInstance() { + @Override + public Path getLocation(String repositoryId) { + return locationResolver.create(repositoryId); + } + + @Override + public void setLocation(String repositoryId, Path location) { + } + } + ); when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation)); when(locationResolver.remove(anyString())).thenAnswer(invocation -> basePath.resolve(invocation.getArgument(0).toString())); } diff --git a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java index 1dfc22e15a..77b09ff707 100644 --- a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java +++ b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java @@ -1,7 +1,6 @@ package sonia.scm; import sonia.scm.repository.BasicRepositoryLocationResolver; -import sonia.scm.repository.RepositoryLocationResolver; import java.io.File; import java.nio.file.Path; @@ -16,6 +15,16 @@ public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationRe @Override protected RepositoryLocationResolverInstance create(Class type) { - return repositoryId -> (T) tempDirectory.toPath(); + return new RepositoryLocationResolverInstance() { + @Override + public T getLocation(String repositoryId) { + return (T) tempDirectory.toPath(); + } + + @Override + public void setLocation(String repositoryId, T location) { + throw new UnsupportedOperationException("not implemented for tests"); + } + }; } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java index 3362496330..f067fa799b 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java @@ -1,14 +1,20 @@ package sonia.scm.update; import com.google.inject.servlet.ServletModule; -import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; - -import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; class MigrationWizardModule extends ServletModule { + private static final Logger LOG = LoggerFactory.getLogger(MigrationWizardModule.class); + @Override protected void configureServlets() { + LOG.info("=========================================================="); + LOG.info("= ="); + LOG.info("= STARTING MIGRATION SERVLET ="); + LOG.info("= ="); + LOG.info("=========================================================="); serve("/*").with(MigrationWizardServlet.class); } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java index 060ed6704e..89394060f5 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java @@ -1,5 +1,7 @@ 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; @@ -10,6 +12,8 @@ import java.nio.file.Path; class CopyMigrationStrategy extends BaseMigrationStrategy { + private static final Logger LOG = LoggerFactory.getLogger(CopyMigrationStrategy.class); + private final RepositoryLocationResolver locationResolver; @Inject @@ -24,6 +28,7 @@ class CopyMigrationStrategy extends BaseMigrationStrategy { Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); Path sourceDataPath = getSourceDataPath(name, type); + LOG.info("copying repository data from {} to {}", sourceDataPath, targetDataPath); copyData(sourceDataPath, targetDataPath); return repositoryBasePath; } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java index 62dd67d86a..2f891fff71 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java @@ -1,7 +1,10 @@ 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.nio.file.Files; @@ -9,16 +12,23 @@ import java.nio.file.Path; class InlineMigrationStrategy extends BaseMigrationStrategy { + private static final Logger LOG = LoggerFactory.getLogger(InlineMigrationStrategy.class); + + private final RepositoryLocationResolver locationResolver; + @Inject - public InlineMigrationStrategy(SCMContextProvider contextProvider) { + public InlineMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) { super(contextProvider); + this.locationResolver = locationResolver; } @Override public Path migrate(String id, String name, String type) { Path repositoryBasePath = getSourceDataPath(name, type); + locationResolver.forClass(Path.class).setLocation(id, repositoryBasePath); Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); + LOG.info("moving repository data from {} to {}", repositoryBasePath, targetDataPath); moveData(repositoryBasePath, targetDataPath); return repositoryBasePath; } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java index 08d71dd376..c571b8ad4c 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java @@ -32,6 +32,7 @@ class MoveMigrationStrategy extends BaseMigrationStrategy { Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); Path sourceDataPath = getSourceDataPath(name, type); + LOG.info("moving repository data from {} to {}", sourceDataPath, targetDataPath); moveData(sourceDataPath, targetDataPath); deleteOldDataDir(getTypeDependentPath(type), name); return repositoryBasePath; diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index acb28ca432..b7037d0e05 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -157,6 +157,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { private Path handleDataDirectory(V1Repository v1Repository) { MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository); + LOG.info("using strategy {} to migrate repository {} with id {}", dataMigrationStrategy.getClass(), v1Repository.name, v1Repository.id); return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type); } @@ -231,6 +232,10 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { return name; } + public String getType() { + return type; + } + public String getNewNamespace() { String[] nameParts = getNameParts(name); return nameParts.length > 1 ? nameParts[0] : type; diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 641e718136..07447bedbc 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -11,6 +11,7 @@ You have migrated from SCM-Manager v1 to SCM-Manager v2. + @@ -19,6 +20,9 @@ You have migrated from SCM-Manager v1 to SCM-Manager v2. + diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java index 6abddae3fb..fa237e78b0 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java @@ -7,11 +7,14 @@ import org.junitpioneer.jupiter.TempDirectory; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver; import java.io.IOException; import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(TempDirectory.class) @@ -20,9 +23,14 @@ class InlineMigrationStrategyTest { @Mock SCMContextProvider contextProvider; + @Mock + PathBasedRepositoryLocationResolver locationResolver; + @Mock + RepositoryLocationResolver.RepositoryLocationResolverInstance locationResolverInstance; @BeforeEach void mockContextProvider(@TempDirectory.TempDir Path tempDir) { + when(locationResolver.forClass(Path.class)).thenReturn(locationResolverInstance); when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); } @@ -33,13 +41,14 @@ class InlineMigrationStrategyTest { @Test void shouldUseExistingDirectory(@TempDirectory.TempDir Path tempDir) { - Path target = new InlineMigrationStrategy(contextProvider).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + Path target = new InlineMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); assertThat(target).isEqualTo(resolveOldDirectory(tempDir)); + verify(locationResolverInstance).setLocation("b4f-a9f0-49f7-ad1f-37d3aae1c55f", target); } @Test void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) { - new InlineMigrationStrategy(contextProvider).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + new InlineMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); assertThat(resolveOldDirectory(tempDir).resolve("data")).exists(); } From 9a1d80327ee3528543036cfc02f0a353b426a9bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 5 Jun 2019 10:24:52 +0200 Subject: [PATCH 04/57] Delete old repository data directories for inline --- .../update/repository/InlineMigrationStrategy.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java index 2f891fff71..60f03666a5 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java @@ -7,6 +7,7 @@ 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; @@ -34,6 +35,10 @@ class InlineMigrationStrategy extends BaseMigrationStrategy { } private void moveData(Path sourceDirectory, Path targetDirectory) { + moveData(sourceDirectory, targetDirectory, false); + } + + private void moveData(Path sourceDirectory, Path targetDirectory, boolean deleteDirectory) { createDataDirectory(targetDirectory); listSourceDirectory(sourceDirectory) .filter(sourceFile -> !targetDirectory.equals(sourceFile)) @@ -41,11 +46,18 @@ class InlineMigrationStrategy extends BaseMigrationStrategy { sourceFile -> { Path targetFile = targetDirectory.resolve(sourceFile.getFileName()); if (Files.isDirectory(sourceFile)) { - moveData(sourceFile, targetFile); + moveData(sourceFile, targetFile, true); } else { moveFile(sourceFile, targetFile); } } ); + if (deleteDirectory) { + try { + Files.delete(sourceDirectory); + } catch (IOException e) { + LOG.warn("could not delete source repository directory {}", sourceDirectory); + } + } } } From a5c65b4e2ca1597be62a4568b135f9e56aeac57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 5 Jun 2019 10:45:41 +0200 Subject: [PATCH 05/57] Store absolute path for directly set repository locations --- .../repository/xml/PathBasedRepositoryLocationResolver.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java index dccdbc197b..c8c462be36 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -78,7 +78,7 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation @Override public void setLocation(String repositoryId, T location) { - PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, (Path) location); + PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, ((Path) location).toAbsolutePath()); } }; } @@ -148,7 +148,7 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation .resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION)); } - public void setLocation(String repositoryId, Path repositoryBasePath) { + private void setLocation(String repositoryId, Path repositoryBasePath) { pathById.put(repositoryId, repositoryBasePath); writePathDatabase(); } From c7875e7f78ba81768b30571ac65712a4647c8183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 5 Jun 2019 11:52:36 +0200 Subject: [PATCH 06/57] Style pages --- .../scm/update/MigrationWizardModule.java | 4 + .../scm/update/MigrationWizardServlet.java | 4 +- .../repository-migration-restart.mustache | 24 ++++- .../templates/repository-migration.mustache | 91 ++++++++++++------- 4 files changed, 88 insertions(+), 35 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java index f067fa799b..55b1644bb8 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java @@ -3,6 +3,8 @@ package sonia.scm.update; import com.google.inject.servlet.ServletModule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.PushStateDispatcher; +import sonia.scm.WebResourceServlet; class MigrationWizardModule extends ServletModule { @@ -15,6 +17,8 @@ class MigrationWizardModule extends ServletModule { LOG.info("= STARTING MIGRATION SERVLET ="); LOG.info("= ="); LOG.info("=========================================================="); + bind(PushStateDispatcher.class).toInstance((request, response, uri) -> {}); + serve("/images/*", "/styles/*").with(WebResourceServlet.class); serve("/*").with(MigrationWizardServlet.class); } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index d65a8408a0..7eb11cff8a 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -15,6 +15,7 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -45,6 +46,7 @@ class MigrationWizardServlet extends HttpServlet { HashMap model = new HashMap<>(); + model.put("contextPath", req.getContextPath()); model.put("submitUrl", req.getRequestURI()); model.put("repositories", missingMigrationStrategies); model.put("strategies", getMigrationStrategies()); @@ -68,7 +70,7 @@ class MigrationWizardServlet extends HttpServlet { MustacheFactory mf = new DefaultMustacheFactory(); Mustache mustache = mf.compile("templates/repository-migration-restart.mustache"); - mustache.execute(resp.getWriter(), new Object()).flush(); + mustache.execute(resp.getWriter(), Collections.singletonMap("contextPath", req.getContextPath())).flush(); ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data")); } diff --git a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache index e47945174a..2374861fe2 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache @@ -2,9 +2,31 @@ SCM-Manager Restart + -SCM-Manager will restart to migrate the data. +
+
+
+
+
+
+
+
SCM-Manager
+
+
+
+
+
+
+
+

SCM-Manager will restart to migrate the data.

+
+
+
+
+
+
diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 07447bedbc..bfc421cd35 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -2,41 +2,66 @@ SCM-Manager Migration + -

SCM-Manager Migration

-You have migrated from SCM-Manager v1 to SCM-Manager v2. -
-
original nametype new namespace/name Strategy
{{name}} + {{type}} + {{newNamespace}}/{{newName}}
- - - - - - - {{#repositories}} - - - - - - - {{/repositories}} -
original nametypenew namespace/nameStrategy
- {{name}} - - {{type}} - - {{newNamespace}}/{{newName}} - - -
- - +
+
+
+
+
+
+
+
SCM-Manager
+
+
+
+
+
+
+
+

SCM-Manager Migration

+

You have migrated from SCM-Manager v1 to SCM-Manager v2.

+
+ + + + + + + + {{#repositories}} + + + + + + + {{/repositories}} +
Original nameTypeNew namespace and nameStrategy
+ {{name}} + + {{type}} + + {{newNamespace}}/{{newName}} + +
+
+ +
+
+
+ +
+
+
+
+
+
From b274952fa91703ca9f983506047ba31d8cc258aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 5 Jun 2019 14:27:35 +0200 Subject: [PATCH 07/57] Create explicit method to create new repository locations --- .../repository/RepositoryLocationResolver.java | 18 ++++++++++++++++++ .../PathBasedRepositoryLocationResolver.java | 15 ++++++++++++++- ...athBasedRepositoryLocationResolverTest.java | 12 ++++++------ .../repository/xml/XmlRepositoryDAOTest.java | 5 +++++ .../scm/TempDirRepositoryLocationResolver.java | 5 +++++ .../SimpleRepositoryHandlerTestBase.java | 8 +++++--- .../repository/CopyMigrationStrategy.java | 2 +- .../repository/MoveMigrationStrategy.java | 2 +- .../repository/CopyMigrationStrategyTest.java | 2 +- .../repository/MoveMigrationStrategyTest.java | 2 +- 10 files changed, 57 insertions(+), 14 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java index 8c76bdabaa..1b7da51c4c 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java @@ -14,8 +14,26 @@ public abstract class RepositoryLocationResolver { } public interface RepositoryLocationResolverInstance { + + /** + * Get the existing location for the repository. + * @param repositoryId The id of the repository. + * @throws IllegalStateException when there is no known location for the given repository. + */ T getLocation(String repositoryId); + /** + * Create a new location for the new repository. + * @param repositoryId The id of the new repository. + * @throws IllegalStateException when there already is a location for the given repository registered. + */ + T createLocation(String repositoryId); + + /** + * Set the location of a new repository. + * @param repositoryId The id of the new repository. + * @throws IllegalStateException when there already is a location for the given repository registered. + */ void setLocation(String repositoryId, T location); } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java index c8c462be36..96067ba7a2 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -71,6 +71,15 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation public T getLocation(String repositoryId) { if (pathById.containsKey(repositoryId)) { return (T) contextProvider.resolve(pathById.get(repositoryId)); + } else { + throw new IllegalStateException("location for repository " + repositoryId + " does not exist"); + } + } + + @Override + public T createLocation(String repositoryId) { + if (pathById.containsKey(repositoryId)) { + throw new IllegalStateException("location for repository " + repositoryId + " already exists"); } else { return (T) create(repositoryId); } @@ -78,7 +87,11 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation @Override public void setLocation(String repositoryId, T location) { - PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, ((Path) location).toAbsolutePath()); + if (pathById.containsKey(repositoryId)) { + throw new IllegalStateException("location for repository " + repositoryId + " already exists"); + } else { + PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, ((Path) location).toAbsolutePath()); + } } }; } diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java index 5f758f124a..754b8469d5 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java @@ -57,7 +57,7 @@ class PathBasedRepositoryLocationResolverTest { @Test void shouldCreateInitialDirectory() { - Path path = resolver.forClass(Path.class).getLocation("newId"); + Path path = resolver.forClass(Path.class).createLocation("newId"); assertThat(path).isEqualTo(basePath.resolve("newId")); assertThat(path).isDirectory(); @@ -65,7 +65,7 @@ class PathBasedRepositoryLocationResolverTest { @Test void shouldPersistInitialDirectory() { - resolver.forClass(Path.class).getLocation("newId"); + resolver.forClass(Path.class).createLocation("newId"); String content = getXmlFileContent(); @@ -78,7 +78,7 @@ class PathBasedRepositoryLocationResolverTest { long now = CREATION_TIME + 100; when(clock.millis()).thenReturn(now); - resolver.forClass(Path.class).getLocation("newId"); + resolver.forClass(Path.class).createLocation("newId"); assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME); @@ -91,7 +91,7 @@ class PathBasedRepositoryLocationResolverTest { long now = CREATION_TIME + 100; when(clock.millis()).thenReturn(now); - resolver.forClass(Path.class).getLocation("newId"); + resolver.forClass(Path.class).createLocation("newId"); assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME); assertThat(resolver.getLastModified()).isEqualTo(now); @@ -108,8 +108,8 @@ class PathBasedRepositoryLocationResolverTest { @BeforeEach void createExistingDatabase() { - resolver.forClass(Path.class).getLocation("existingId_1"); - resolver.forClass(Path.class).getLocation("existingId_2"); + resolver.forClass(Path.class).createLocation("existingId_1"); + resolver.forClass(Path.class).createLocation("existingId_2"); resolverWithExistingData = createResolver(); } diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java index 36c9db33e2..9ab8925fdb 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java @@ -64,6 +64,11 @@ class XmlRepositoryDAOTest { return locationResolver.create(repositoryId); } + @Override + public Path createLocation(String repositoryId) { + return locationResolver.create(repositoryId); + } + @Override public void setLocation(String repositoryId, Path location) { } diff --git a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java index 77b09ff707..acffe6c769 100644 --- a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java +++ b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java @@ -21,6 +21,11 @@ public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationRe return (T) tempDirectory.toPath(); } + @Override + public T createLocation(String repositoryId) { + return (T) tempDirectory.toPath(); + } + @Override public void setLocation(String repositoryId, T location) { throw new UnsupportedOperationException("not implemented for tests"); diff --git a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java index 063681bbc4..615169b58c 100644 --- a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java +++ b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java @@ -34,6 +34,7 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- import org.junit.Test; +import org.mockito.stubbing.Answer; import sonia.scm.AbstractTestBase; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.InMemoryConfigurationStoreFactory; @@ -82,11 +83,12 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase { RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class); when(locationResolver.create(any())).thenReturn(instanceMock); when(locationResolver.supportsLocationType(any())).thenReturn(true); - - when(instanceMock.getLocation(anyString())).then(ic -> { + Answer pathAnswer = ic -> { String id = ic.getArgument(0); return baseDirectory.toPath().resolve(id); - }); + }; + when(instanceMock.getLocation(anyString())).then(pathAnswer); + when(instanceMock.createLocation(anyString())).then(pathAnswer); handler = createRepositoryHandler(storeFactory, locationResolver, baseDirectory); } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java index 89394060f5..f96413d195 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java @@ -24,7 +24,7 @@ class CopyMigrationStrategy extends BaseMigrationStrategy { @Override public Path migrate(String id, String name, String type) { - Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id); + Path repositoryBasePath = locationResolver.forClass(Path.class).createLocation(id); Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); Path sourceDataPath = getSourceDataPath(name, type); diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java index c571b8ad4c..deb8a8782b 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java @@ -28,7 +28,7 @@ class MoveMigrationStrategy extends BaseMigrationStrategy { @Override public Path migrate(String id, String name, String type) { - Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id); + Path repositoryBasePath = locationResolver.forClass(Path.class).createLocation(id); Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); Path sourceDataPath = getSourceDataPath(name, type); diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java index b40283ae79..d718554dfe 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java @@ -43,7 +43,7 @@ class CopyMigrationStrategyTest { void mockLocationResolver(@TempDirectory.TempDir Path tempDir) { RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class); when(locationResolver.forClass(Path.class)).thenReturn(instanceMock); - when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0))); + when(instanceMock.createLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0))); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java index b55315d85f..e248f82217 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java @@ -40,7 +40,7 @@ class MoveMigrationStrategyTest { void mockLocationResolver(@TempDirectory.TempDir Path tempDir) { RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class); when(locationResolver.forClass(Path.class)).thenReturn(instanceMock); - when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0))); + when(instanceMock.createLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0))); } @Test From 67f731c43206d7541d6ff2ebcaf6211288a65123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 5 Jun 2019 15:39:36 +0200 Subject: [PATCH 08/57] Heed sonar hints --- .../scm/update/MigrationWizardServlet.java | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index 7eb11cff8a..e451e60120 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -3,6 +3,8 @@ package sonia.scm.update; import com.github.mustachejava.DefaultMustacheFactory; import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.boot.RestartEvent; import sonia.scm.event.ScmEventBus; import sonia.scm.update.repository.MigrationStrategy; @@ -15,9 +17,11 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.PrintWriter; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; import static java.util.Arrays.stream; import static java.util.stream.Collectors.toList; @@ -25,6 +29,8 @@ import static java.util.stream.Collectors.toList; @Singleton class MigrationWizardServlet extends HttpServlet { + private static final Logger LOG = LoggerFactory.getLogger(MigrationWizardServlet.class); + private final XmlRepositoryV1UpdateStep repositoryV1UpdateStep; private final MigrationStrategyDao migrationStrategyDao; @@ -42,8 +48,6 @@ class MigrationWizardServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { List missingMigrationStrategies = repositoryV1UpdateStep.missingMigrationStrategies(); - resp.setStatus(200); - HashMap model = new HashMap<>(); model.put("contextPath", req.getContextPath()); @@ -52,8 +56,8 @@ class MigrationWizardServlet extends HttpServlet { model.put("strategies", getMigrationStrategies()); MustacheFactory mf = new DefaultMustacheFactory(); - Mustache mustache = mf.compile("templates/repository-migration.mustache"); - mustache.execute(resp.getWriter(), model).flush(); + Mustache template = mf.compile("templates/repository-migration.mustache"); + respondWithTemplate(resp, model, template); } private List getMigrationStrategies() { @@ -61,7 +65,7 @@ class MigrationWizardServlet extends HttpServlet { } @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { resp.setStatus(200); req.getParameterMap().forEach( @@ -69,9 +73,25 @@ class MigrationWizardServlet extends HttpServlet { ); MustacheFactory mf = new DefaultMustacheFactory(); - Mustache mustache = mf.compile("templates/repository-migration-restart.mustache"); - mustache.execute(resp.getWriter(), Collections.singletonMap("contextPath", req.getContextPath())).flush(); + Mustache template = mf.compile("templates/repository-migration-restart.mustache"); + Map model = Collections.singletonMap("contextPath", req.getContextPath()); + + respondWithTemplate(resp, model, template); ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data")); } + + private void respondWithTemplate(HttpServletResponse resp, Map model, Mustache template) { + PrintWriter writer; + try { + writer = resp.getWriter(); + } catch (IOException e) { + LOG.error("could not create writer for response", e); + resp.setStatus(500); + return; + } + template.execute(writer, model); + writer.flush(); + resp.setStatus(200); + } } From 77a1ad50fea1758b8a00ab15c79752f6ef9b5fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 10:45:56 +0200 Subject: [PATCH 09/57] Refresh repository dao after repository.xml file was renamed Without this, the XmlRepositoryDAO will be initialized at a time where there is no repository-paths.xml file. Therefore the dao cannot initialize with the existing repositories whose paths are kept in repositories.xml at that time. In this commit we trigger a refresh after the file was renamed, so that the PathBasedRepositoryLocationResolver can read the moved repository-paths.xml file and all repositories will be found. --- .../PathBasedRepositoryLocationResolver.java | 8 +- .../scm/repository/xml/XmlRepositoryDAO.java | 7 ++ .../repository/xml/XmlRepositoryDAOTest.java | 104 ++++++++++++------ .../XmlRepositoryFileNameUpdateStep.java | 6 +- .../XmlRepositoryFileNameUpdateStepTest.java | 9 +- 5 files changed, 94 insertions(+), 40 deletions(-) diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java index a1ce516069..bfb59f9f35 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -7,6 +7,7 @@ import sonia.scm.repository.InternalRepositoryException; import sonia.scm.store.StoreConstants; import javax.inject.Inject; +import javax.inject.Singleton; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -28,6 +29,7 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity; * * @since 2.0.0 */ +@Singleton public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver { public static final String STORE_NAME = "repository-paths"; @@ -48,7 +50,7 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC()); } - public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) { + PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) { super(Path.class); this.contextProvider = contextProvider; this.initialRepositoryLocationResolver = initialRepositoryLocationResolver; @@ -138,4 +140,8 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation .resolve(StoreConstants.CONFIG_DIRECTORY_NAME) .resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION)); } + + public void refresh() { + this.read(); + } } 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 f506793d1a..151e8f1281 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 @@ -198,4 +198,11 @@ public class XmlRepositoryDAO implements RepositoryDAO { public Long getLastModified() { return repositoryLocationResolver.getLastModified(); } + + public void refresh() { + repositoryLocationResolver.refresh(); + byNamespaceAndName.clear(); + byId.clear(); + init(); + } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java index 5b9a00aec8..d77bfb3c63 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java @@ -8,8 +8,6 @@ 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.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; @@ -32,7 +30,9 @@ import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -47,9 +47,6 @@ class XmlRepositoryDAOTest { @Mock private PathBasedRepositoryLocationResolver locationResolver; - @Captor - private ArgumentCaptor> forAllCaptor; - private FileSystem fileSystem = new DefaultFileSystem(); private XmlRepositoryDAO dao; @@ -268,43 +265,80 @@ class XmlRepositoryDAOTest { verify(locationResolver).updateModificationDate(); } - } - @Test - void shouldReadExistingRepositoriesFromPathDatabase(@TempDirectory.TempDir Path basePath) throws IOException { - doNothing().when(locationResolver).forAllPaths(forAllCaptor.capture()); - XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem); + private String getXmlFileContent(String id) { + Path storePath = metadataFile(id); - Path repositoryPath = basePath.resolve("existing"); - Files.createDirectories(repositoryPath); - URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml"); - Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml")); + assertThat(storePath).isRegularFile(); + return content(storePath); + } - forAllCaptor.getValue().accept("existing", repositoryPath); + private Path metadataFile(String id) { + return locationResolver.create(id).resolve("metadata.xml"); + } - assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue(); - } - - private String getXmlFileContent(String id) { - Path storePath = metadataFile(id); - - assertThat(storePath).isRegularFile(); - return content(storePath); - } - - private Path metadataFile(String id) { - return locationResolver.create(id).resolve("metadata.xml"); - } - - private String content(Path storePath) { - try { - return new String(Files.readAllBytes(storePath), Charsets.UTF_8); - } catch (IOException e) { - throw new RuntimeException(e); + private String content(Path storePath) { + try { + return new String(Files.readAllBytes(storePath), Charsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } } } - private static Repository createRepository(String id) { + @Nested + class WithExistingRepositories { + + private Path repositoryPath; + + @BeforeEach + void createMetadataFileForRepository(@TempDirectory.TempDir Path basePath) throws IOException { + repositoryPath = basePath.resolve("existing"); + + Files.createDirectories(repositoryPath); + URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml"); + Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml")); + } + + @Test + void shouldReadExistingRepositoriesFromPathDatabase() { + // given + mockExistingPath(); + + // when + XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem); + + // then + assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue(); + } + + @Test + void shouldRefreshWithExistingRepositoriesFromPathDatabase() { + // given + doNothing().when(locationResolver).forAllPaths(any()); + XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem); + + mockExistingPath(); + + // when + dao.refresh(); + + // then + verify(locationResolver).refresh(); + assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue(); + } + + private void mockExistingPath() { + doAnswer( + invocation -> { + ((BiConsumer) invocation.getArgument(0)).accept("existing", repositoryPath); + return null; + } + ).when(locationResolver).forAllPaths(any()); + } + } + + private Repository createRepository(String id) { return new Repository(id, "xml", "space", id); } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryFileNameUpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryFileNameUpdateStep.java index 62902713f0..9df6f81440 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryFileNameUpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryFileNameUpdateStep.java @@ -6,6 +6,7 @@ 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; @@ -27,10 +28,12 @@ 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) { + public XmlRepositoryFileNameUpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO repositoryDAO) { this.contextProvider = contextProvider; + this.repositoryDAO = repositoryDAO; } @Override @@ -41,6 +44,7 @@ public class XmlRepositoryFileNameUpdateStep implements UpdateStep { if (Files.exists(oldRepositoriesFile)) { LOG.info("moving old repositories database files to repository-paths file"); Files.move(oldRepositoriesFile, newRepositoryPathsFile); + repositoryDAO.refresh(); } } diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryFileNameUpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryFileNameUpdateStepTest.java index 51be47fc82..658330956d 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryFileNameUpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryFileNameUpdateStepTest.java @@ -7,8 +7,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junitpioneer.jupiter.TempDirectory; import sonia.scm.SCMContextProvider; import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver; +import sonia.scm.repository.xml.XmlRepositoryDAO; -import javax.xml.bind.JAXBException; import java.io.IOException; import java.net.URL; import java.nio.file.Files; @@ -16,12 +16,14 @@ import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(TempDirectory.class) class XmlRepositoryFileNameUpdateStepTest { SCMContextProvider contextProvider = mock(SCMContextProvider.class); + XmlRepositoryDAO repositoryDAO = mock(XmlRepositoryDAO.class); @BeforeEach void mockScmHome(@TempDirectory.TempDir Path tempDir) { @@ -29,8 +31,8 @@ class XmlRepositoryFileNameUpdateStepTest { } @Test - void shouldCopyRepositoriesFileToRepositoryPathsFile(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException { - XmlRepositoryFileNameUpdateStep updateStep = new XmlRepositoryFileNameUpdateStep(contextProvider); + void shouldCopyRepositoriesFileToRepositoryPathsFile(@TempDirectory.TempDir Path tempDir) throws IOException { + XmlRepositoryFileNameUpdateStep updateStep = new XmlRepositoryFileNameUpdateStep(contextProvider, repositoryDAO); URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml"); Path configDir = tempDir.resolve("config"); Files.createDirectories(configDir); @@ -40,5 +42,6 @@ class XmlRepositoryFileNameUpdateStepTest { assertThat(configDir.resolve(PathBasedRepositoryLocationResolver.STORE_NAME + ".xml")).exists(); assertThat(configDir.resolve("repositories.xml")).doesNotExist(); + verify(repositoryDAO).refresh(); } } From 1bcc150cb90a794bf8f7aa32d82a6e49086077ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 11:15:51 +0200 Subject: [PATCH 10/57] Remove function creep --- .../java/sonia/scm/ScmContextListener.java | 18 +++++++++++++++--- .../scm/boot/BootstrapContextListener.java | 14 -------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java index 5cb3f5dfa5..874052442d 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java @@ -58,6 +58,8 @@ import sonia.scm.util.IOUtil; import javax.inject.Inject; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; +import java.io.Closeable; +import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Set; @@ -77,7 +79,7 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList private final ClassLoader parent; private final Set plugins; private Injector injector; - + public interface Factory { ScmContextListener create(ClassLoader parent, Set plugins); } @@ -183,6 +185,18 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList } super.contextDestroyed(servletContextEvent); + + for (PluginWrapper plugin : getPlugins()) { + ClassLoader pcl = plugin.getClassLoader(); + + if (pcl instanceof Closeable) { + try { + ((Closeable) pcl).close(); + } catch (IOException ex) { + LOG.warn("could not close plugin classloader", ex); + } + } + } } private void closeCloseables() { @@ -205,6 +219,4 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList private void destroyServletContextListeners(ServletContextEvent event) { injector.getInstance(ServletContextListenerHolder.class).contextDestroyed(event); } - - } diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index be7955ea28..40eb7b2496 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -112,20 +112,6 @@ public class BootstrapContextListener implements ServletContextListener { public void contextDestroyed(ServletContextEvent sce) { contextListener.contextDestroyed(sce); - if (contextListener instanceof ScmContextListener) { - for (PluginWrapper plugin : ((ScmContextListener) contextListener).getPlugins()) { - ClassLoader pcl = plugin.getClassLoader(); - - if (pcl instanceof Closeable) { - try { - ((Closeable) pcl).close(); - } catch (IOException ex) { - logger.warn("could not close plugin classloader", ex); - } - } - } - } - context = null; contextListener = null; } From ec538039f97662f43e520df8358b4f3f409e1963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 12:47:32 +0200 Subject: [PATCH 11/57] Cleanup --- .../scm/boot/BootstrapContextListener.java | 36 +++++++++++-------- .../scm/update/MigrationWizardModule.java | 2 ++ .../scm/update/MigrationWizardServlet.java | 18 +++++----- .../repository/XmlRepositoryV1UpdateStep.java | 2 +- 4 files changed, 34 insertions(+), 24 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index 40eb7b2496..76aad46b77 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -141,35 +141,43 @@ public class BootstrapContextListener implements ServletContextListener { } private void createContextListener(File pluginDirectory) { + ClassLoader cl; + Set plugins; + PluginLoader pluginLoader; try { renameOldPluginsFolder(pluginDirectory); + if (!isCorePluginExtractionDisabled()) { extractCorePlugins(context, pluginDirectory); } else { logger.info("core plugin extraction is disabled"); } - ClassLoader cl = ClassLoaders.getContextClassLoader(BootstrapContextListener.class); + cl = ClassLoaders.getContextClassLoader(BootstrapContextListener.class); - Set plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath()); + plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath()); - PluginLoader pluginLoader = new DefaultPluginLoader(context, cl, plugins); + pluginLoader = new DefaultPluginLoader(context, cl, plugins); - Injector bootstrapInjector = createBootstrapInjector(pluginLoader); - - MigrationWizardContextListener wizardContextListener = prepareWizardIfNeeded(bootstrapInjector); - - if (wizardContextListener.wizardNecessary()) { - contextListener = wizardContextListener; - } else { - processUpdates(pluginLoader, bootstrapInjector); - - contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins); - } } catch (IOException ex) { throw new PluginLoadException("could not load plugins", ex); } + + Injector bootstrapInjector = createBootstrapInjector(pluginLoader); + + startEitherMigrationOrNormalServlet(cl, plugins, pluginLoader, bootstrapInjector); + } + + private void startEitherMigrationOrNormalServlet(ClassLoader cl, Set plugins, PluginLoader pluginLoader, Injector bootstrapInjector) { + MigrationWizardContextListener wizardContextListener = prepareWizardIfNeeded(bootstrapInjector); + + if (wizardContextListener.wizardNecessary()) { + contextListener = wizardContextListener; + } else { + processUpdates(pluginLoader, bootstrapInjector); + contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins); + } } private void renameOldPluginsFolder(File pluginDirectory) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java index 55b1644bb8..b2c4e842a3 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java @@ -16,6 +16,8 @@ class MigrationWizardModule extends ServletModule { LOG.info("= ="); LOG.info("= STARTING MIGRATION SERVLET ="); LOG.info("= ="); + LOG.info("= Open SCM-Manager in a browser to start the wizard. ="); + LOG.info("= ="); LOG.info("=========================================================="); bind(PushStateDispatcher.class).toInstance((request, response, uri) -> {}); serve("/images/*", "/styles/*").with(WebResourceServlet.class); diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index e451e60120..53245565bd 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -24,7 +24,6 @@ import java.util.List; import java.util.Map; import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toList; @Singleton class MigrationWizardServlet extends HttpServlet { @@ -41,18 +40,19 @@ class MigrationWizardServlet extends HttpServlet { } public boolean wizardNecessary() { - return !repositoryV1UpdateStep.missingMigrationStrategies().isEmpty(); + return !repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies().isEmpty(); } @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - List missingMigrationStrategies = repositoryV1UpdateStep.missingMigrationStrategies(); + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + List repositoriesWithoutMigrationStrategies = + repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies(); HashMap model = new HashMap<>(); model.put("contextPath", req.getContextPath()); model.put("submitUrl", req.getRequestURI()); - model.put("repositories", missingMigrationStrategies); + model.put("repositories", repositoriesWithoutMigrationStrategies); model.put("strategies", getMigrationStrategies()); MustacheFactory mf = new DefaultMustacheFactory(); @@ -60,10 +60,6 @@ class MigrationWizardServlet extends HttpServlet { respondWithTemplate(resp, model, template); } - private List getMigrationStrategies() { - return stream(MigrationStrategy.values()).map(Enum::name).collect(toList()); - } - @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { resp.setStatus(200); @@ -81,6 +77,10 @@ class MigrationWizardServlet extends HttpServlet { ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data")); } + private MigrationStrategy[] getMigrationStrategies() { + return MigrationStrategy.values(); + } + private void respondWithTemplate(HttpServletResponse resp, Map model, Mustache template) { PrintWriter writer; try { diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index b7037d0e05..aeb74971a9 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -111,7 +111,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { ); } - public List missingMigrationStrategies() { + public List getRepositoriesWithoutMigrationStrategies() { if (!resolveV1File().exists()) { LOG.info("no v1 repositories database file found"); return emptyList(); From 1065899e9966c0c8c528876bcd33a45579703a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 12:52:10 +0200 Subject: [PATCH 12/57] Add missing test --- .../XmlRepositoryV1UpdateStepTest.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java index 2102d296ea..b07f7f786c 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java @@ -221,6 +221,25 @@ class XmlRepositoryV1UpdateStepTest { assertThat(tempDir.resolve("config").resolve("repositories.xml.v1.backup")).doesNotExist(); } + @Test + void shouldGetNoMissingStrategiesWithFormerV2DatabaseFile(@TempDirectory.TempDir Path tempDir) throws IOException { + createFormerV2RepositoriesFile(tempDir); + + assertThat(updateStep.getRepositoriesWithoutMigrationStrategies()).isEmpty(); + } + + @Test + void shouldFindMissingStrategies(@TempDirectory.TempDir Path tempDir) throws IOException { + V1RepositoryFileSystem.createV1Home(tempDir); + + assertThat(updateStep.getRepositoriesWithoutMigrationStrategies()) + .extracting("id") + .contains( + "3b91caa5-59c3-448f-920b-769aaa56b761", + "c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f", + "454972da-faf9-4437-b682-dc4a4e0aa8eb"); + } + private void createFormerV2RepositoriesFile(@TempDirectory.TempDir Path tempDir) throws IOException { URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml"); Path configDir = tempDir.resolve("config"); From 748043f537c79d86c74abe44d1b2342e1df6c1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 12:52:23 +0200 Subject: [PATCH 13/57] Describe different migration strategies --- .../update/repository/MigrationStrategy.java | 23 +++++++++++++++---- .../templates/repository-migration.mustache | 14 ++++++++++- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java index d4b6c5c0f5..f3de48cfd9 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java @@ -6,14 +6,27 @@ import java.nio.file.Path; public enum MigrationStrategy { - COPY(CopyMigrationStrategy.class), - MOVE(MoveMigrationStrategy.class), - INLINE(InlineMigrationStrategy.class); + COPY(CopyMigrationStrategy.class, + "Copy the repository data files to the new native location inside SCM-Manager home directory. " + + "This will keep the original directory."), + MOVE(MoveMigrationStrategy.class, + "Move the repository data files to the new native location inside SCM-Manager home directory. " + + "The original directory will be deleted."), + INLINE(InlineMigrationStrategy.class, + "Use the current directory where the repository data files are stored, but modify the directory " + + "structure so that it can be used for SCM-Manager v2. The repository data files will be moved to a new " + + "subdirectory 'data' inside the current directory."); - private Class implementationClass; + private final Class implementationClass; + private final String description; - MigrationStrategy(Class implementationClass) { + MigrationStrategy(Class implementationClass, String description) { this.implementationClass = implementationClass; + this.description = description; + } + + public String getDescription() { + return description; } Instance from(Injector injector) { diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index bfc421cd35..067d4b3413 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -47,7 +47,7 @@
@@ -58,6 +58,18 @@ +
+
+

These are the different strategies:

+ + {{#strategies}} + + + + + {{/strategies}} +
{{name}}{{description}}
+
From c39c14bbd15a6abfa8a2c720eee50b15f44215c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 13:31:40 +0200 Subject: [PATCH 14/57] Remove no longer needed LfsStoreRemoveListener With v2 the LFS store resides inside the repository directory that is purged completely on deletion. Therefore an explicit deletion of the LFS folder is no longer necessary. --- .../scm/web/lfs/LfsStoreRemoveListener.java | 97 ------------------- 1 file changed, 97 deletions(-) delete mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsStoreRemoveListener.java diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsStoreRemoveListener.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsStoreRemoveListener.java deleted file mode 100644 index 18fb333c74..0000000000 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsStoreRemoveListener.java +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - -package sonia.scm.web.lfs; - -import com.github.legman.Subscribe; -import com.google.inject.Inject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.EagerSingleton; -import sonia.scm.HandlerEventType; -import sonia.scm.plugin.Extension; -import sonia.scm.repository.GitRepositoryHandler; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryEvent; -import sonia.scm.store.Blob; -import sonia.scm.store.BlobStore; - -/** - * Listener which removes all lfs objects from a blob store, whenever its corresponding git repository gets deleted. - * - * @author Sebastian Sdorra - * @since 1.54 - */ -@Extension -@EagerSingleton -public class LfsStoreRemoveListener { - - private static final Logger LOG = LoggerFactory.getLogger(LfsBlobStoreFactory.class); - - private final LfsBlobStoreFactory lfsBlobStoreFactory; - - @Inject - public LfsStoreRemoveListener(LfsBlobStoreFactory lfsBlobStoreFactory) { - this.lfsBlobStoreFactory = lfsBlobStoreFactory; - } - - /** - * Remove all object from the blob store, if the event is an delete event and the repository is a git repository. - * - * @param event repository event - */ - @Subscribe - public void handleRepositoryEvent(RepositoryEvent event) { - if ( isDeleteEvent(event) && isGitRepositoryEvent(event) ) { - removeLfsStore(event.getItem()); - } - } - - private boolean isDeleteEvent(RepositoryEvent event) { - return HandlerEventType.DELETE == event.getEventType(); - } - - private boolean isGitRepositoryEvent(RepositoryEvent event) { - return event.getItem() != null - && event.getItem().getType().equals(GitRepositoryHandler.TYPE_NAME); - } - - private void removeLfsStore(Repository repository) { - LOG.debug("remove all blobs from store, because corresponding git repository {} was removed", repository.getName()); - BlobStore blobStore = lfsBlobStoreFactory.getLfsBlobStore(repository); - for ( Blob blob : blobStore.getAll() ) { - LOG.trace("remove blob {}, because repository {} was removed", blob.getId(), repository.getName()); - blobStore.remove(blob); - } - } - -} From 815c5312204edb02faa3485ff56138146bba9fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 13:42:57 +0200 Subject: [PATCH 15/57] Use file system class to create repository directory --- .../xml/PathBasedRepositoryLocationResolver.java | 15 ++++++++------- .../PathBasedRepositoryLocationResolverTest.java | 6 +++++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java index 96067ba7a2..3055b34931 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -1,6 +1,7 @@ package sonia.scm.repository.xml; import sonia.scm.SCMContextProvider; +import sonia.scm.io.FileSystem; import sonia.scm.repository.BasicRepositoryLocationResolver; import sonia.scm.repository.InitialRepositoryLocationResolver; import sonia.scm.repository.InternalRepositoryException; @@ -8,8 +9,6 @@ import sonia.scm.store.StoreConstants; import javax.inject.Inject; import javax.inject.Singleton; -import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.time.Clock; import java.util.Map; @@ -36,6 +35,7 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation private final SCMContextProvider contextProvider; private final InitialRepositoryLocationResolver initialRepositoryLocationResolver; + private final FileSystem fileSystem; private final PathDatabase pathDatabase; private final Map pathById; @@ -46,14 +46,15 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation private Long lastModified; @Inject - public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver) { - this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC()); + public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem) { + this(contextProvider, initialRepositoryLocationResolver, fileSystem, Clock.systemUTC()); } - public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) { + PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem, Clock clock) { super(Path.class); this.contextProvider = contextProvider; this.initialRepositoryLocationResolver = initialRepositoryLocationResolver; + this.fileSystem = fileSystem; this.pathById = new ConcurrentHashMap<>(); this.clock = clock; @@ -101,8 +102,8 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation setLocation(repositoryId, path); Path resolvedPath = contextProvider.resolve(path); try { - Files.createDirectories(resolvedPath); - } catch (IOException e) { + fileSystem.create(resolvedPath.toFile()); + } catch (Exception e) { throw new InternalRepositoryException(entity("Repository", repositoryId), "could not create directory for new repository", e); } return resolvedPath; diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java index 754b8469d5..941775d6ea 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java @@ -11,6 +11,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import sonia.scm.SCMContextProvider; +import sonia.scm.io.DefaultFileSystem; +import sonia.scm.io.FileSystem; import sonia.scm.repository.InitialRepositoryLocationResolver; import java.io.IOException; @@ -41,6 +43,8 @@ class PathBasedRepositoryLocationResolverTest { @Mock private Clock clock; + private final FileSystem fileSystem = new DefaultFileSystem(); + private Path basePath; private PathBasedRepositoryLocationResolver resolver; @@ -159,7 +163,7 @@ class PathBasedRepositoryLocationResolverTest { } private PathBasedRepositoryLocationResolver createResolver() { - return new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver, clock); + return new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver, fileSystem, clock); } private String content(Path storePath) { From 1288724d6a9a5f6af3d375f0893af06af9d13c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 13:46:17 +0200 Subject: [PATCH 16/57] Remove test without implementation --- .../web/lfs/LfsStoreRemoveListenerTest.java | 122 ------------------ 1 file changed, 122 deletions(-) delete mode 100644 scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsStoreRemoveListenerTest.java diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsStoreRemoveListenerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsStoreRemoveListenerTest.java deleted file mode 100644 index 34a1a3f4d5..0000000000 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsStoreRemoveListenerTest.java +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - -package sonia.scm.web.lfs; - -import com.google.common.collect.Lists; -import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import static org.mockito.Mockito.*; -import org.mockito.junit.MockitoJUnitRunner; -import sonia.scm.HandlerEventType; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryEvent; -import sonia.scm.repository.RepositoryTestData; -import sonia.scm.store.Blob; -import sonia.scm.store.BlobStore; - -/** - * Unit tests for {@link LfsStoreRemoveListener}. - * - * @author Sebastian Sdorra - */ -@RunWith(MockitoJUnitRunner.class) -public class LfsStoreRemoveListenerTest { - - @Mock - private LfsBlobStoreFactory lfsBlobStoreFactory; - - @Mock - private BlobStore blobStore; - - @InjectMocks - private LfsStoreRemoveListener lfsStoreRemoveListener; - - @Test - public void testHandleRepositoryEventWithNonDeleteEvents() { - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_CREATE)); - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.CREATE)); - - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_MODIFY)); - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.MODIFY)); - - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_DELETE)); - - verifyZeroInteractions(lfsBlobStoreFactory); - } - - @Test - public void testHandleRepositoryEventWithNonGitRepositories() { - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "svn")); - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "hg")); - lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "dummy")); - - verifyZeroInteractions(lfsBlobStoreFactory); - } - - @Test - public void testHandleRepositoryEvent() { - Repository heartOfGold = RepositoryTestData.createHeartOfGold("git"); - - when(lfsBlobStoreFactory.getLfsBlobStore(heartOfGold)).thenReturn(blobStore); - Blob blobA = mockBlob("a"); - Blob blobB = mockBlob("b"); - List blobs = Lists.newArrayList(blobA, blobB); - when(blobStore.getAll()).thenReturn(blobs); - - - lfsStoreRemoveListener.handleRepositoryEvent(new RepositoryEvent(HandlerEventType.DELETE, heartOfGold)); - verify(blobStore).getAll(); - verify(blobStore).remove(blobA); - verify(blobStore).remove(blobB); - - verifyNoMoreInteractions(blobStore); - } - - private Blob mockBlob(String id) { - Blob blob = mock(Blob.class); - when(blob.getId()).thenReturn(id); - return blob; - } - - private RepositoryEvent event(HandlerEventType eventType) { - return event(eventType, "git"); - } - - private RepositoryEvent event(HandlerEventType eventType, String repositoryType) { - return new RepositoryEvent(eventType, RepositoryTestData.create42Puzzle(repositoryType)); - } - -} From 47413de44a79001eb4d09770fe0b14998a359a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 14:50:01 +0200 Subject: [PATCH 17/57] Try to load SCM-Manager after migration --- .../repository-migration-restart.mustache | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache index 2374861fe2..1ff8a1baa6 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache @@ -24,9 +24,69 @@

SCM-Manager will restart to migrate the data.

+
+

+ + + + + + + + + + + + +

+
+ From 20acd4ca646febb595b3292f0c6246c7abb7785b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 7 Jun 2019 13:02:17 +0200 Subject: [PATCH 18/57] Restructure migration page --- .../templates/repository-migration.mustache | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 067d4b3413..26b1650e68 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -21,8 +21,26 @@
-

SCM-Manager Migration

-

You have migrated from SCM-Manager v1 to SCM-Manager v2.

+

+

SCM-Manager Migration

+

You have migrated from SCM-Manager v1 to SCM-Manager v2.

+

+ To migrate the existing repositories you have to specify a namespace and a name for each on them + as well as a migration strategy. +

+

+ The strategies are the following: + + {{#strategies}} + + + + + {{/strategies}} +
{{name}}{{description}}
+

+
+
@@ -34,7 +52,7 @@ {{#repositories}}
- {{name}} + {{type}}/{{name}} {{type}} @@ -58,18 +76,6 @@
-
-
-

These are the different strategies:

- - {{#strategies}} - - - - - {{/strategies}} -
{{name}}{{description}}
-
From 8a6b57e06c998a81000fe23c6cce8d30b351a800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 7 Jun 2019 13:52:54 +0200 Subject: [PATCH 19/57] Add new migration strategies "delete" and "ignore" --- .../repository/CopyMigrationStrategy.java | 7 ++-- .../repository/DeleteMigrationStrategy.java | 32 +++++++++++++++++++ .../repository/IgnoreMigrationStrategy.java | 13 ++++++++ .../repository/InlineMigrationStrategy.java | 7 ++-- .../update/repository/MigrationStrategy.java | 15 +++++++-- .../repository/MoveMigrationStrategy.java | 6 ++-- .../repository/XmlRepositoryV1UpdateStep.java | 30 +++++++++-------- .../repository/CopyMigrationStrategyTest.java | 4 +-- .../InlineMigrationStrategyTest.java | 2 +- .../repository/MigrationStrategyMock.java | 10 ++++++ .../repository/MoveMigrationStrategyTest.java | 4 +-- .../XmlRepositoryV1UpdateStepTest.java | 18 ++++++++++- 12 files changed, 121 insertions(+), 27 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/update/repository/DeleteMigrationStrategy.java create mode 100644 scm-webapp/src/main/java/sonia/scm/update/repository/IgnoreMigrationStrategy.java diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java index f96413d195..b66b2ea9c9 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java @@ -9,6 +9,9 @@ import sonia.scm.repository.RepositoryLocationResolver; import javax.inject.Inject; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Optional; + +import static java.util.Optional.of; class CopyMigrationStrategy extends BaseMigrationStrategy { @@ -23,14 +26,14 @@ class CopyMigrationStrategy extends BaseMigrationStrategy { } @Override - public Path migrate(String id, String name, String type) { + public Optional migrate(String id, String name, String type) { Path repositoryBasePath = locationResolver.forClass(Path.class).createLocation(id); Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); Path sourceDataPath = getSourceDataPath(name, type); LOG.info("copying repository data from {} to {}", sourceDataPath, targetDataPath); copyData(sourceDataPath, targetDataPath); - return repositoryBasePath; + return of(repositoryBasePath); } private void copyData(Path sourceDirectory, Path targetDirectory) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/DeleteMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/DeleteMigrationStrategy.java new file mode 100644 index 0000000000..cf61152613 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/DeleteMigrationStrategy.java @@ -0,0 +1,32 @@ +package sonia.scm.update.repository; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.util.IOUtil; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +public class DeleteMigrationStrategy extends BaseMigrationStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(DeleteMigrationStrategy.class); + + @Inject + DeleteMigrationStrategy(SCMContextProvider contextProvider) { + super(contextProvider); + } + + @Override + public Optional migrate(String id, String name, String type) { + Path sourceDataPath = getSourceDataPath(name, type); + try { + IOUtil.delete(sourceDataPath.toFile(), true); + } catch (IOException e) { + LOG.warn("could not delete old repository path for repository {} with type {} and id {}", name, type, id); + } + return Optional.empty(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/IgnoreMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/IgnoreMigrationStrategy.java new file mode 100644 index 0000000000..945538009a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/IgnoreMigrationStrategy.java @@ -0,0 +1,13 @@ +package sonia.scm.update.repository; + +import java.nio.file.Path; +import java.util.Optional; + +import static java.util.Optional.empty; + +public class IgnoreMigrationStrategy implements MigrationStrategy.Instance { + @Override + public Optional migrate(String id, String name, String type) { + return empty(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java index 60f03666a5..a75ff5e571 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java @@ -10,6 +10,9 @@ import javax.inject.Inject; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Optional; + +import static java.util.Optional.of; class InlineMigrationStrategy extends BaseMigrationStrategy { @@ -24,14 +27,14 @@ class InlineMigrationStrategy extends BaseMigrationStrategy { } @Override - public Path migrate(String id, String name, String type) { + public Optional migrate(String id, String name, String type) { Path repositoryBasePath = getSourceDataPath(name, type); locationResolver.forClass(Path.class).setLocation(id, repositoryBasePath); Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); LOG.info("moving repository data from {} to {}", repositoryBasePath, targetDataPath); moveData(repositoryBasePath, targetDataPath); - return repositoryBasePath; + return of(repositoryBasePath); } private void moveData(Path sourceDirectory, Path targetDirectory) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java index f3de48cfd9..46604d327e 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java @@ -3,6 +3,7 @@ package sonia.scm.update.repository; import com.google.inject.Injector; import java.nio.file.Path; +import java.util.Optional; public enum MigrationStrategy { @@ -15,7 +16,13 @@ public enum MigrationStrategy { INLINE(InlineMigrationStrategy.class, "Use the current directory where the repository data files are stored, but modify the directory " + "structure so that it can be used for SCM-Manager v2. The repository data files will be moved to a new " + - "subdirectory 'data' inside the current directory."); + "subdirectory 'data' inside the current directory."), + IGNORE(IgnoreMigrationStrategy.class, + "The repository will not be migrated and will not be visible inside SCM-Manager. " + + "The data files will be kept at the current location."), + DELETE(DeleteMigrationStrategy.class, + "The repository will not be migrated and will not be visible inside SCM-Manager. " + + "The data files will be deleted!"); private final Class implementationClass; private final String description; @@ -25,6 +32,10 @@ public enum MigrationStrategy { this.description = description; } + public Class getImplementationClass() { + return implementationClass; + } + public String getDescription() { return description; } @@ -34,6 +45,6 @@ public enum MigrationStrategy { } interface Instance { - Path migrate(String id, String name, String type); + Optional migrate(String id, String name, String type); } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java index deb8a8782b..5b364d8bc3 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java @@ -11,8 +11,10 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Optional; import static java.util.Arrays.asList; +import static java.util.Optional.of; class MoveMigrationStrategy extends BaseMigrationStrategy { @@ -27,7 +29,7 @@ class MoveMigrationStrategy extends BaseMigrationStrategy { } @Override - public Path migrate(String id, String name, String type) { + public Optional migrate(String id, String name, String type) { Path repositoryBasePath = locationResolver.forClass(Path.class).createLocation(id); Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); @@ -35,7 +37,7 @@ class MoveMigrationStrategy extends BaseMigrationStrategy { LOG.info("moving repository data from {} to {}", sourceDataPath, targetDataPath); moveData(sourceDataPath, targetDataPath); deleteOldDataDir(getTypeDependentPath(type), name); - return repositoryBasePath; + return of(repositoryBasePath); } private void deleteOldDataDir(Path rootPath, String name) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index aeb74971a9..0d40a1b413 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -141,21 +141,25 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { } private void update(V1Repository v1Repository) { - Path destination = handleDataDirectory(v1Repository); - Repository repository = new Repository( - v1Repository.id, - v1Repository.type, - v1Repository.getNewNamespace(), - v1Repository.getNewName(), - 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); + Optional destination = handleDataDirectory(v1Repository); + destination.ifPresent( + newPath -> { + Repository repository = new Repository( + v1Repository.id, + v1Repository.type, + v1Repository.getNewNamespace(), + v1Repository.getNewName(), + v1Repository.contact, + v1Repository.description, + createPermissions(v1Repository)); + LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.name, newPath); + repositoryDao.add(repository, newPath); + propertyStore.put(v1Repository.id, v1Repository.properties); + } + ); } - private Path handleDataDirectory(V1Repository v1Repository) { + private Optional handleDataDirectory(V1Repository v1Repository) { MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository); LOG.info("using strategy {} to migrate repository {} with id {}", dataMigrationStrategy.getClass(), v1Repository.name, v1Repository.id); return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type); diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java index d718554dfe..d3f18a1ff7 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java @@ -48,13 +48,13 @@ class CopyMigrationStrategyTest { @Test void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) { - Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get(); assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f")); } @Test void shouldCopyDataDirectory(@TempDirectory.TempDir Path tempDir) { - Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get(); assertThat(target.resolve("data")).exists(); Path originalDataDir = tempDir .resolve("repositories") diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java index fa237e78b0..2b97c1d79b 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java @@ -41,7 +41,7 @@ class InlineMigrationStrategyTest { @Test void shouldUseExistingDirectory(@TempDirectory.TempDir Path tempDir) { - Path target = new InlineMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + Path target = new InlineMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get(); assertThat(target).isEqualTo(resolveOldDirectory(tempDir)); verify(locationResolverInstance).setLocation("b4f-a9f0-49f7-ad1f-37d3aae1c55f", target); } diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyMock.java b/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyMock.java index e0018f584f..c04c9477bb 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyMock.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyMock.java @@ -3,10 +3,13 @@ package sonia.scm.update.repository; import com.google.inject.Injector; import sonia.scm.update.repository.MigrationStrategy.Instance; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; +import static java.util.Optional.of; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -20,6 +23,13 @@ class MigrationStrategyMock { .thenAnswer( invocationOnMock -> mocks.computeIfAbsent(invocationOnMock.getArgument(0), key -> mock((Class) key)) ); + + for (MigrationStrategy strategy : MigrationStrategy.values()) { + MigrationStrategy.Instance strategyMock = mock(strategy.getImplementationClass()); + when(strategyMock.migrate(any(), any(), any())).thenReturn(of(Paths.get(""))); + lenient().when(mock.getInstance((Class) strategy.getImplementationClass())).thenReturn(strategyMock); + } + return mock; } } diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java index e248f82217..fa58eb8ea1 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java @@ -45,13 +45,13 @@ class MoveMigrationStrategyTest { @Test void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) { - Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get(); assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f")); } @Test void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) { - Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get(); assertThat(target.resolve("data")).exists(); Path originalDataDir = tempDir .resolve("repositories") diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java index b07f7f786c..76a74993b3 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Optional; import static java.util.Optional.empty; @@ -33,10 +34,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static sonia.scm.update.repository.MigrationStrategy.COPY; +import static sonia.scm.update.repository.MigrationStrategy.DELETE; import static sonia.scm.update.repository.MigrationStrategy.INLINE; import static sonia.scm.update.repository.MigrationStrategy.MOVE; @@ -177,13 +180,26 @@ class XmlRepositoryV1UpdateStepTest { void shouldUseDirectoryFromStrategy(@TempDirectory.TempDir Path tempDir) throws JAXBException { Path targetDir = tempDir.resolve("someDir"); MigrationStrategy.Instance strategyMock = injectorMock.getInstance(InlineMigrationStrategy.class); - when(strategyMock.migrate("454972da-faf9-4437-b682-dc4a4e0aa8eb", "simple", "git")).thenReturn(targetDir); + when(strategyMock.migrate("454972da-faf9-4437-b682-dc4a4e0aa8eb", "simple", "git")).thenReturn(of(targetDir)); updateStep.doUpdate(); assertThat(locationCaptor.getAllValues()).contains(targetDir); } + @Test + void shouldSkipWhenStrategyGivesNoNewPath() throws JAXBException { + for (MigrationStrategy strategy : MigrationStrategy.values()) { + MigrationStrategy.Instance strategyMock = mock(strategy.getImplementationClass()); + lenient().when(strategyMock.migrate(any(), any(), any())).thenReturn(empty()); + lenient().when(injectorMock.getInstance((Class) strategy.getImplementationClass())).thenReturn(strategyMock); + } + + updateStep.doUpdate(); + + assertThat(locationCaptor.getAllValues()).isEmpty(); + } + @Test void shouldFailForMissingMigrationStrategy() { lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(empty()); From e5809a6350e158a432215881de548e6a32be9cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 7 Jun 2019 13:59:31 +0200 Subject: [PATCH 20/57] Sort repositories by type and name --- .../main/java/sonia/scm/update/MigrationWizardServlet.java | 5 ++++- .../scm/update/repository/XmlRepositoryV1UpdateStep.java | 4 ++++ .../main/resources/templates/repository-migration.mustache | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index 53245565bd..9db24dc8eb 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -18,7 +18,9 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -46,7 +48,8 @@ class MigrationWizardServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { List repositoriesWithoutMigrationStrategies = - repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies(); + new ArrayList<>(repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies()); + repositoriesWithoutMigrationStrategies.sort(Comparator.comparing(XmlRepositoryV1UpdateStep.V1Repository::getPath)); HashMap model = new HashMap<>(); diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index 0d40a1b413..54d0d7c52f 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -240,6 +240,10 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { return type; } + public String getPath() { + return type + "/" + name; + } + public String getNewNamespace() { String[] nameParts = getNameParts(name); return nameParts.length > 1 ? nameParts[0] : type; diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 26b1650e68..83d2fe94dd 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -52,7 +52,7 @@ {{#repositories}} - {{type}}/{{name}} + {{path}} {{type}} From 70de4d729260ec2d6bbc2c5a4224d602eb1859f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 7 Jun 2019 13:59:50 +0200 Subject: [PATCH 21/57] Organize imports --- .../src/main/java/sonia/scm/update/MigrationWizardServlet.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index 9db24dc8eb..6a11896008 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -25,8 +25,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import static java.util.Arrays.stream; - @Singleton class MigrationWizardServlet extends HttpServlet { From df9a3c12dd86071293418a02de4c90008fcacb50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 7 Jun 2019 14:16:23 +0200 Subject: [PATCH 22/57] First step to make name and namespace editable in migration --- .../scm/update/MigrationWizardServlet.java | 10 +++++++-- .../repository/MigrationStrategyDao.java | 4 ++-- .../repository/RepositoryMigrationPlan.java | 22 ++++++++++++++++++- .../templates/repository-migration.mustache | 13 ++++++++--- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index 6a11896008..66c1006d4f 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -19,6 +19,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -65,8 +66,13 @@ class MigrationWizardServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) { resp.setStatus(200); - req.getParameterMap().forEach( - (name, strategy) -> migrationStrategyDao.set(name, MigrationStrategy.valueOf(strategy[0])) + Arrays.stream(req.getParameterValues("ids")).forEach( + id -> { + String strategy = req.getParameter("strategy-" + id); + String namespace = req.getParameter("namespace-" + id); + String name = req.getParameter("name-" + id); + migrationStrategyDao.set(id, MigrationStrategy.valueOf(strategy), namespace, name); + } ); MustacheFactory mf = new DefaultMustacheFactory(); diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java index 8ddb5e02df..07c049bc84 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java @@ -23,8 +23,8 @@ public class MigrationStrategyDao { return plan.get(id); } - public void set(String repositoryId, MigrationStrategy strategy) { - plan.set(repositoryId, strategy); + public void set(String repositoryId, MigrationStrategy strategy, String newNamespace, String newName) { + plan.set(repositoryId, strategy, newNamespace, newName); store.set(plan); } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java b/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java index f2b2dc9788..c839eba5b0 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java @@ -28,10 +28,12 @@ class RepositoryMigrationPlan { .map(RepositoryEntry::getDataMigrationStrategy); } - public void set(String repositoryId, MigrationStrategy strategy) { + public void set(String repositoryId, MigrationStrategy strategy, String newNamespace, String newName) { Optional entry = findEntry(repositoryId); if (entry.isPresent()) { entry.get().setStrategy(strategy); + entry.get().setNewNamespace(newNamespace); + entry.get().setNewName(newName); } else { entries.add(new RepositoryEntry(repositoryId, strategy)); } @@ -49,6 +51,8 @@ class RepositoryMigrationPlan { private String repositoryId; private MigrationStrategy dataMigrationStrategy; + private String newNamespace; + private String newName; RepositoryEntry() { } @@ -62,8 +66,24 @@ class RepositoryMigrationPlan { return dataMigrationStrategy; } + public String getNewNamespace() { + return newNamespace; + } + + public String getNewName() { + return newName; + } + private void setStrategy(MigrationStrategy strategy) { this.dataMigrationStrategy = strategy; } + + private void setNewNamespace(String newNamespace) { + this.newNamespace = newNamespace; + } + + private void setNewName(String newName) { + this.newName = newName; + } } } diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 83d2fe94dd..5a9085fb4b 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -46,7 +46,8 @@ Original name Type - New namespace and name + New namespace + New name Strategy {{#repositories}} @@ -58,12 +59,15 @@ {{type}} - {{newNamespace}}/{{newName}} + + + +
- {{#strategies}} {{/strategies}} @@ -74,6 +78,9 @@ {{/repositories}} + {{#repositories}} + + {{/repositories}}
From 13951595c4f89b3bbfa28c0c9100e3f45181dccc Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 7 Jun 2019 12:34:10 +0000 Subject: [PATCH 23/57] Close branch bugfix/refresh_repo_db_after_upgrade From fdb8143b766c2b42482576c82778c63104af40af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 11 Jun 2019 10:27:21 +0200 Subject: [PATCH 24/57] Validate new namespace and name on migration --- .../sonia/scm/util/ValidationUtilTest.java | 7 +- .../scm/update/MigrationWizardServlet.java | 133 ++++++++++++++++-- .../templates/repository-migration.mustache | 23 +-- 3 files changed, 139 insertions(+), 24 deletions(-) diff --git a/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java b/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java index 972ed95a2d..51ff906b8f 100644 --- a/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java +++ b/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java @@ -147,6 +147,10 @@ public class ValidationUtilTest public void testIsRepositoryNameValid() { String[] validPaths = { "scm", + "scm-", + "scm_", + "s_cm", + "s-cm", "s", "sc", ".hiddenrepo", @@ -206,7 +210,8 @@ public class ValidationUtilTest "a/..b", "scm/main", "scm/plugins/git-plugin", - "scm/plugins/git-plugin" + "_scm", + "-scm" }; for (String path : validPaths) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index 66c1006d4f..cd5f640c7b 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -10,6 +10,7 @@ import sonia.scm.event.ScmEventBus; import sonia.scm.update.repository.MigrationStrategy; import sonia.scm.update.repository.MigrationStrategyDao; import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; +import sonia.scm.util.ValidationUtil; import javax.inject.Inject; import javax.inject.Singleton; @@ -18,13 +19,13 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.Comparator.comparing; @Singleton class MigrationWizardServlet extends HttpServlet { @@ -46,16 +47,20 @@ class MigrationWizardServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { - List repositoriesWithoutMigrationStrategies = - new ArrayList<>(repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies()); - repositoriesWithoutMigrationStrategies.sort(Comparator.comparing(XmlRepositoryV1UpdateStep.V1Repository::getPath)); + List repositoryLineEntries = getRepositoryLineEntries(); + doGet(req, resp, repositoryLineEntries); + } + protected void doGet(HttpServletRequest req, HttpServletResponse resp, List repositoryLineEntries) { HashMap model = new HashMap<>(); model.put("contextPath", req.getContextPath()); model.put("submitUrl", req.getRequestURI()); - model.put("repositories", repositoriesWithoutMigrationStrategies); + model.put("repositories", repositoryLineEntries); model.put("strategies", getMigrationStrategies()); + model.put("validationErrorsFound", repositoryLineEntries + .stream() + .anyMatch(entry -> entry.isNamespaceInvalid() || entry.isNameInvalid())); MustacheFactory mf = new DefaultMustacheFactory(); Mustache template = mf.compile("templates/repository-migration.mustache"); @@ -64,16 +69,41 @@ class MigrationWizardServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { - resp.setStatus(200); + List repositoryLineEntries = getRepositoryLineEntries(); - Arrays.stream(req.getParameterValues("ids")).forEach( - id -> { - String strategy = req.getParameter("strategy-" + id); - String namespace = req.getParameter("namespace-" + id); - String name = req.getParameter("name-" + id); - migrationStrategyDao.set(id, MigrationStrategy.valueOf(strategy), namespace, name); + boolean validationErrorFound = false; + for (RepositoryLineEntry repositoryLineEntry : repositoryLineEntries) { + String id = repositoryLineEntry.getId(); + String namespace = req.getParameter("namespace-" + id); + String name = req.getParameter("name-" + id); + repositoryLineEntry.setNamespace(namespace); + repositoryLineEntry.setName(name); + + if (!ValidationUtil.isRepositoryNameValid(namespace)) { + repositoryLineEntry.setNamespaceValid(false); + validationErrorFound = true; } - ); + if (!ValidationUtil.isRepositoryNameValid(name)) { + repositoryLineEntry.setNameValid(false); + validationErrorFound = true; + } + } + + if (validationErrorFound) { + doGet(req, resp, repositoryLineEntries); + return; + } + + repositoryLineEntries.stream() + .map(RepositoryLineEntry::getId) + .forEach( + id -> { + String strategy = req.getParameter("strategy-" + id); + String namespace = req.getParameter("namespace-" + id); + String name = req.getParameter("name-" + id); + migrationStrategyDao.set(id, MigrationStrategy.valueOf(strategy), namespace, name); + } + ); MustacheFactory mf = new DefaultMustacheFactory(); Mustache template = mf.compile("templates/repository-migration-restart.mustache"); @@ -81,9 +111,20 @@ class MigrationWizardServlet extends HttpServlet { respondWithTemplate(resp, model, template); + resp.setStatus(200); + ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data")); } + private List getRepositoryLineEntries() { + List repositoriesWithoutMigrationStrategies = + repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies(); + return repositoriesWithoutMigrationStrategies.stream() + .map(RepositoryLineEntry::new) + .sorted(comparing(RepositoryLineEntry::getPath)) + .collect(Collectors.toList()); + } + private MigrationStrategy[] getMigrationStrategies() { return MigrationStrategy.values(); } @@ -101,4 +142,66 @@ class MigrationWizardServlet extends HttpServlet { writer.flush(); resp.setStatus(200); } + + private static class RepositoryLineEntry { + private final String id; + private final String type; + private final String path; + private String namespace; + private String name; + private boolean namespaceValid = true; + private boolean nameValid = true; + + public RepositoryLineEntry(XmlRepositoryV1UpdateStep.V1Repository repository) { + this.id = repository.getId(); + this.type = repository.getType(); + this.path = repository.getPath(); + this.namespace = repository.getNewNamespace(); + this.name = repository.getNewName(); + } + + public String getId() { + return id; + } + + public String getType() { + return type; + } + + public String getPath() { + return path; + } + + public String getNamespace() { + return namespace; + } + + public String getName() { + return name; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public void setName(String name) { + this.name = name; + } + + public void setNamespaceValid(boolean namespaceValid) { + this.namespaceValid = namespaceValid; + } + + public void setNameValid(boolean nameValid) { + this.nameValid = nameValid; + } + + public boolean isNamespaceInvalid() { + return !namespaceValid; + } + + public boolean isNameInvalid() { + return !nameValid; + } + } } diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 5a9085fb4b..9d7e5e0a0b 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -41,14 +41,24 @@


+ {{#validationErrorsFound}} +
Please correct the invalid namespaces or names below and try again.
+
+ {{/validationErrorsFound}}
- - - + + + {{#repositories}} @@ -59,10 +69,10 @@ {{type}} {{/repositories}}
Original name TypeNew namespaceNew nameStrategyNew namespace + + New name + + Strategy + +
- + - +
@@ -78,9 +88,6 @@
- {{#repositories}} - - {{/repositories}}
From 802fb3e0cf0b4ca3a490d11f6e44278a55de2b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 11 Jun 2019 13:10:31 +0200 Subject: [PATCH 25/57] Use manually entered namespace and name --- .../scm/update/MigrationWizardServlet.java | 48 +++-- .../repository/MigrationStrategyDao.java | 2 +- .../repository/RepositoryMigrationPlan.java | 43 ++-- .../scm/update/repository/V1Permission.java | 25 +++ .../scm/update/repository/V1Repository.java | 92 +++++++++ .../repository/XmlRepositoryV1UpdateStep.java | 126 +++--------- .../update/MigrationWizardServletTest.java | 188 ++++++++++++++++++ .../repository/MigrationStrategyDaoTest.java | 38 ++-- .../XmlRepositoryV1UpdateStepTest.java | 60 ++---- 9 files changed, 427 insertions(+), 195 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/update/repository/V1Permission.java create mode 100644 scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java create mode 100644 scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index cd5f640c7b..f26b1afac5 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -3,12 +3,14 @@ package sonia.scm.update; import com.github.mustachejava.DefaultMustacheFactory; import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheFactory; +import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.boot.RestartEvent; import sonia.scm.event.ScmEventBus; import sonia.scm.update.repository.MigrationStrategy; import sonia.scm.update.repository.MigrationStrategyDao; +import sonia.scm.update.repository.V1Repository; import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; import sonia.scm.util.ValidationUtil; @@ -19,6 +21,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -62,9 +65,7 @@ class MigrationWizardServlet extends HttpServlet { .stream() .anyMatch(entry -> entry.isNamespaceInvalid() || entry.isNameInvalid())); - MustacheFactory mf = new DefaultMustacheFactory(); - Mustache template = mf.compile("templates/repository-migration.mustache"); - respondWithTemplate(resp, model, template); + respondWithTemplate(resp, model, "templates/repository-migration.mustache"); } @Override @@ -76,6 +77,7 @@ class MigrationWizardServlet extends HttpServlet { String id = repositoryLineEntry.getId(); String namespace = req.getParameter("namespace-" + id); String name = req.getParameter("name-" + id); + String strategy = req.getParameter("strategy-" + id); repositoryLineEntry.setNamespace(namespace); repositoryLineEntry.setName(name); @@ -105,19 +107,15 @@ class MigrationWizardServlet extends HttpServlet { } ); - MustacheFactory mf = new DefaultMustacheFactory(); - Mustache template = mf.compile("templates/repository-migration-restart.mustache"); Map model = Collections.singletonMap("contextPath", req.getContextPath()); - respondWithTemplate(resp, model, template); - - resp.setStatus(200); + respondWithTemplate(resp, model, "templates/repository-migration-restart.mustache"); ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data")); } private List getRepositoryLineEntries() { - List repositoriesWithoutMigrationStrategies = + List repositoriesWithoutMigrationStrategies = repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies(); return repositoriesWithoutMigrationStrategies.stream() .map(RepositoryLineEntry::new) @@ -129,7 +127,11 @@ class MigrationWizardServlet extends HttpServlet { return MigrationStrategy.values(); } - private void respondWithTemplate(HttpServletResponse resp, Map model, Mustache template) { + @VisibleForTesting + void respondWithTemplate(HttpServletResponse resp, Map model, String templateName) { + MustacheFactory mf = new DefaultMustacheFactory(); + Mustache template = mf.compile(templateName); + PrintWriter writer; try { writer = resp.getWriter(); @@ -152,12 +154,30 @@ class MigrationWizardServlet extends HttpServlet { private boolean namespaceValid = true; private boolean nameValid = true; - public RepositoryLineEntry(XmlRepositoryV1UpdateStep.V1Repository repository) { + public RepositoryLineEntry(V1Repository repository) { this.id = repository.getId(); this.type = repository.getType(); - this.path = repository.getPath(); - this.namespace = repository.getNewNamespace(); - this.name = repository.getNewName(); + this.path = repository.getType() + "/" + repository.getName(); + this.namespace = computeNewNamespace(repository); + this.name = computeNewName(repository); + } + + private static String computeNewNamespace(V1Repository v1Repository) { + String[] nameParts = getNameParts(v1Repository.getName()); + return nameParts.length > 1 ? nameParts[0] : v1Repository.getType(); + } + + private static String computeNewName(V1Repository v1Repository) { + String[] nameParts = getNameParts(v1Repository.getName()); + return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts); + } + + private static String[] getNameParts(String v1Name) { + return v1Name.split("/"); + } + + private static String concatPathElements(String[] nameParts) { + return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_")); } public String getId() { diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java index 07c049bc84..5670ce458a 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java @@ -19,7 +19,7 @@ public class MigrationStrategyDao { this.plan = store.getOptional().orElse(new RepositoryMigrationPlan()); } - public Optional get(String id) { + public Optional get(String id) { return plan.get(id); } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java b/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java index c839eba5b0..3c4bf36342 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java @@ -13,53 +13,50 @@ import static java.util.Arrays.asList; @XmlRootElement(name = "repository-migration") class RepositoryMigrationPlan { - private List entries; + private List entries; RepositoryMigrationPlan() { - this(new RepositoryEntry[0]); + this(new RepositoryMigrationEntry[0]); } - RepositoryMigrationPlan(RepositoryEntry... entries) { + RepositoryMigrationPlan(RepositoryMigrationEntry... entries) { this.entries = new ArrayList<>(asList(entries)); } - Optional get(String repositoryId) { - return findEntry(repositoryId) - .map(RepositoryEntry::getDataMigrationStrategy); - } - - public void set(String repositoryId, MigrationStrategy strategy, String newNamespace, String newName) { - Optional entry = findEntry(repositoryId); - if (entry.isPresent()) { - entry.get().setStrategy(strategy); - entry.get().setNewNamespace(newNamespace); - entry.get().setNewName(newName); - } else { - entries.add(new RepositoryEntry(repositoryId, strategy)); - } - } - - private Optional findEntry(String repositoryId) { + Optional get(String repositoryId) { return entries.stream() .filter(repositoryEntry -> repositoryId.equals(repositoryEntry.repositoryId)) .findFirst(); } + public void set(String repositoryId, MigrationStrategy strategy, String newNamespace, String newName) { + Optional entry = get(repositoryId); + if (entry.isPresent()) { + entry.get().setStrategy(strategy); + entry.get().setNewNamespace(newNamespace); + entry.get().setNewName(newName); + } else { + entries.add(new RepositoryMigrationEntry(repositoryId, strategy, newNamespace, newName)); + } + } + @XmlRootElement(name = "entries") @XmlAccessorType(XmlAccessType.FIELD) - static class RepositoryEntry { + static class RepositoryMigrationEntry { private String repositoryId; private MigrationStrategy dataMigrationStrategy; private String newNamespace; private String newName; - RepositoryEntry() { + RepositoryMigrationEntry() { } - RepositoryEntry(String repositoryId, MigrationStrategy dataMigrationStrategy) { + RepositoryMigrationEntry(String repositoryId, MigrationStrategy dataMigrationStrategy, String newNamespace, String newName) { this.repositoryId = repositoryId; this.dataMigrationStrategy = dataMigrationStrategy; + this.newNamespace = newNamespace; + this.newName = newName; } public MigrationStrategy getDataMigrationStrategy() { diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/V1Permission.java b/scm-webapp/src/main/java/sonia/scm/update/repository/V1Permission.java new file mode 100644 index 0000000000..97426f1a83 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/V1Permission.java @@ -0,0 +1,25 @@ +package sonia.scm.update.repository; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "permissions") +class V1Permission { + private boolean groupPermission; + private String name; + private String type; + + public boolean isGroupPermission() { + return groupPermission; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java b/scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java new file mode 100644 index 0000000000..256e2ad397 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java @@ -0,0 +1,92 @@ +package sonia.scm.update.repository; + +import sonia.scm.update.properties.V1Properties; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "repositories") +public 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 permissions; + private V1Properties properties; + + public V1Repository() { + } + + public V1Repository(String id, String type, String name) { + this.id = id; + this.type = type; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getContact() { + return contact; + } + + public long getCreationDate() { + return creationDate; + } + + public Long getLastModified() { + return lastModified; + } + + public String getDescription() { + return description; + } + + public boolean isPublic() { + return isPublic; + } + + public boolean isArchived() { + return archived; + } + + public List getPermissions() { + return permissions; + } + + public V1Properties getProperties() { + return 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 + '\'' + + '}'; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index 54d0d7c52f..2f16dc74f6 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -28,7 +28,6 @@ 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; @@ -104,7 +103,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class); readV1Database(jaxbContext).ifPresent( v1Database -> { - v1Database.repositoryList.repositories.forEach(this::readMigrationStrategy); + v1Database.repositoryList.repositories.forEach(this::readMigrationEntry); v1Database.repositoryList.repositories.forEach(this::update); backupOldRepositoriesFile(); } @@ -141,52 +140,56 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { } private void update(V1Repository v1Repository) { - Optional destination = handleDataDirectory(v1Repository); + RepositoryMigrationPlan.RepositoryMigrationEntry repositoryMigrationEntry = readMigrationEntry(v1Repository); + Optional destination = handleDataDirectory(v1Repository, repositoryMigrationEntry.getDataMigrationStrategy()); + LOG.info("using strategy {} to migrate repository {} with id {} using new namespace {} and name {}", + repositoryMigrationEntry.getDataMigrationStrategy().getClass(), + v1Repository.getName(), + v1Repository.getId(), + repositoryMigrationEntry.getNewNamespace(), + repositoryMigrationEntry.getNewName()); destination.ifPresent( newPath -> { Repository repository = new Repository( - v1Repository.id, - v1Repository.type, - v1Repository.getNewNamespace(), - v1Repository.getNewName(), - v1Repository.contact, - v1Repository.description, + v1Repository.getId(), + v1Repository.getType(), + repositoryMigrationEntry.getNewNamespace(), + repositoryMigrationEntry.getNewName(), + v1Repository.getContact(), + v1Repository.getDescription(), createPermissions(v1Repository)); - LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.name, newPath); + LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.getName(), newPath); repositoryDao.add(repository, newPath); - propertyStore.put(v1Repository.id, v1Repository.properties); + propertyStore.put(v1Repository.getId(), v1Repository.getProperties()); } ); } - private Optional handleDataDirectory(V1Repository v1Repository) { - MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository); - LOG.info("using strategy {} to migrate repository {} with id {}", dataMigrationStrategy.getClass(), v1Repository.name, v1Repository.id); - return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type); + private Optional handleDataDirectory(V1Repository v1Repository, MigrationStrategy dataMigrationStrategy) { + return dataMigrationStrategy + .from(injector) + .migrate(v1Repository.getId(), v1Repository.getName(), v1Repository.getType()); } - private MigrationStrategy readMigrationStrategy(V1Repository v1Repository) { + private RepositoryMigrationPlan.RepositoryMigrationEntry readMigrationEntry(V1Repository v1Repository) { return findMigrationStrategy(v1Repository) - .orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name)); + .orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.getId() + " and name " + v1Repository.getName())); } - private Optional findMigrationStrategy(V1Repository v1Repository) { - return migrationStrategyDao.get(v1Repository.id); + private Optional findMigrationStrategy(V1Repository v1Repository) { + return migrationStrategyDao.get(v1Repository.getId()); } private RepositoryPermission[] createPermissions(V1Repository v1Repository) { - if (v1Repository.permissions == null) { - return new RepositoryPermission[0]; - } - return v1Repository.permissions + return v1Repository.getPermissions() .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); + LOG.info("creating permission {} for {}", v1Permission.getType(), v1Permission.getName()); + return new RepositoryPermission(v1Permission.getName(), v1Permission.getType(), v1Permission.isGroupPermission()); } private Optional readV1Database(JAXBContext jaxbContext) throws JAXBException { @@ -205,79 +208,6 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { ).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") - public 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 permissions; - private V1Properties properties; - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public String getType() { - return type; - } - - public String getPath() { - return type + "/" + name; - } - - public String getNewNamespace() { - String[] nameParts = getNameParts(name); - return nameParts.length > 1 ? nameParts[0] : type; - } - - public String getNewName() { - String[] nameParts = getNameParts(name); - return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts); - } - - private String[] getNameParts(String v1Name) { - return v1Name.split("/"); - } - - private String concatPathElements(String[] nameParts) { - return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_")); - } - - @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 repositories; diff --git a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java new file mode 100644 index 0000000000..e40179eb7f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java @@ -0,0 +1,188 @@ +package sonia.scm.update; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.update.repository.MigrationStrategy; +import sonia.scm.update.repository.MigrationStrategyDao; +import sonia.scm.update.repository.V1Repository; +import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collections; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MigrationWizardServletTest { + + @Mock + XmlRepositoryV1UpdateStep updateStep; + @Mock + MigrationStrategyDao migrationStrategyDao; + + @Mock + HttpServletRequest request; + @Mock + HttpServletResponse response; + + String renderedTemplateName; + Map renderedModel; + + MigrationWizardServlet servlet; + + @BeforeEach + void initServlet() { + servlet = new MigrationWizardServlet(updateStep, migrationStrategyDao) { + @Override + void respondWithTemplate(HttpServletResponse resp, Map model, String templateName) { + renderedTemplateName = templateName; + renderedModel = model; + } + }; + } + + @Test + void shouldUseRepositoryTypeAsNamespaceForNamesWithSingleElement() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "simple")) + ); + + servlet.doGet(request, response); + + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("namespace") + .contains("git"); + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("name") + .contains("simple"); + } + + @Test + void shouldUseDirectoriesForNamespaceAndNameForNamesWithTwoElements() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "two/dirs")) + ); + + servlet.doGet(request, response); + + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("namespace") + .contains("two"); + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("name") + .contains("dirs"); + } + + @Test + void shouldUseDirectoriesForNamespaceAndConcatenatedNameForNamesWithMoreThanTwoElements() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "more/than/two/dirs")) + ); + + servlet.doGet(request, response); + + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("namespace") + .contains("more"); + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("name") + .contains("than_two_dirs"); + } + + @Test + void shouldUseTypeAndNameAsPath() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "name")) + ); + + servlet.doGet(request, response); + + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("path") + .contains("git/name"); + } + + @Test + void shouldKeepId() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "name")) + ); + + servlet.doGet(request, response); + + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("id") + .contains("id"); + } + + @Test + void shouldNotBeInvalidAtFirstRequest() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "name")) + ); + + servlet.doGet(request, response); + + assertThat(renderedModel.get("validationErrorsFound")).isEqualTo(false); + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("namespaceInvalid") + .contains(false); + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("nameInvalid") + .contains(false); + } + + @Test + void shouldValidateNamespaceAndNameOnPost() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "name")) + ); + doReturn("invalid namespace").when(request).getParameter("namespace-id"); + doReturn("invalid name").when(request).getParameter("name-id"); + doReturn("COPY").when(request).getParameter("strategy-id"); + + servlet.doPost(request, response); + + assertThat(renderedModel.get("validationErrorsFound")).isEqualTo(true); + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("namespaceInvalid") + .contains(true); + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("nameInvalid") + .contains(true); + } + + @Test + void shouldStoreValidMigration() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "name")) + ); + doReturn("namespace").when(request).getParameter("namespace-id"); + doReturn("name").when(request).getParameter("name-id"); + doReturn("COPY").when(request).getParameter("strategy-id"); + + servlet.doPost(request, response); + + verify(migrationStrategyDao).set("id", MigrationStrategy.COPY, "namespace", "name"); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java index e3fd4457b6..5829f5b98f 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java @@ -11,8 +11,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.SCMContextProvider; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.JAXBConfigurationStoreFactory; -import sonia.scm.update.repository.MigrationStrategy; -import sonia.scm.update.repository.MigrationStrategyDao; import javax.xml.bind.JAXBException; import java.nio.file.Path; @@ -37,23 +35,31 @@ class MigrationStrategyDaoTest { } @Test - void shouldReturnEmptyOptionalWhenStoreIsEmpty() throws JAXBException { + void shouldReturnEmptyOptionalWhenStoreIsEmpty() { MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); - Optional strategy = dao.get("any"); + Optional entry = dao.get("any"); - Assertions.assertThat(strategy).isEmpty(); + Assertions.assertThat(entry).isEmpty(); } @Test - void shouldReturnNewValue() throws JAXBException { + void shouldReturnNewValue() { MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); - dao.set("id", INLINE); + dao.set("id", INLINE, "space", "name"); - Optional strategy = dao.get("id"); + Optional entry = dao.get("id"); - Assertions.assertThat(strategy).contains(INLINE); + Assertions.assertThat(entry) + .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getDataMigrationStrategy) + .contains(INLINE); + Assertions.assertThat(entry) + .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewNamespace) + .contains("space"); + Assertions.assertThat(entry) + .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewName) + .contains("name"); } @Nested @@ -62,16 +68,24 @@ class MigrationStrategyDaoTest { void initExistingDatabase() throws JAXBException { MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); - dao.set("id", INLINE); + dao.set("id", INLINE, "space", "name"); } @Test void shouldFindExistingValue() throws JAXBException { MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); - Optional strategy = dao.get("id"); + Optional entry = dao.get("id"); - Assertions.assertThat(strategy).contains(INLINE); + Assertions.assertThat(entry) + .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getDataMigrationStrategy) + .contains(INLINE); + Assertions.assertThat(entry) + .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewNamespace) + .contains("space"); + Assertions.assertThat(entry) + .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewName) + .contains("name"); } } } diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java index 76a74993b3..6c977a3cb6 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java @@ -11,6 +11,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.xml.XmlRepositoryDAO; @@ -25,7 +26,6 @@ import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Optional; import static java.util.Optional.empty; @@ -38,9 +38,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static sonia.scm.update.repository.MigrationStrategy.COPY; -import static sonia.scm.update.repository.MigrationStrategy.DELETE; -import static sonia.scm.update.repository.MigrationStrategy.INLINE; import static sonia.scm.update.repository.MigrationStrategy.MOVE; @ExtendWith(MockitoExtension.class) @@ -92,9 +89,14 @@ class XmlRepositoryV1UpdateStepTest { @BeforeEach void createMigrationPlan() { - 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)); + Answer planAnswer = invocation -> { + String id = invocation.getArgument(0).toString(); + return of(new RepositoryMigrationPlan.RepositoryMigrationEntry(id, MOVE, "namespace-" + id, "name-" + id)); + }; + + lenient().when(migrationStrategyDao.get("3b91caa5-59c3-448f-920b-769aaa56b761")).thenAnswer(planAnswer); + lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenAnswer(planAnswer); + lenient().when(migrationStrategyDao.get("454972da-faf9-4437-b682-dc4a4e0aa8eb")).thenAnswer(planAnswer); } @Test @@ -107,56 +109,20 @@ class XmlRepositoryV1UpdateStepTest { void shouldMapAttributes() throws JAXBException { updateStep.doUpdate(); - Optional repository = findByNamespace("git"); + Optional repository = findByNamespace("namespace-3b91caa5-59c3-448f-920b-769aaa56b761"); assertThat(repository) .get() .hasFieldOrPropertyWithValue("type", "git") .hasFieldOrPropertyWithValue("contact", "arthur@dent.uk") - .hasFieldOrPropertyWithValue("description", "A simple repository without directories."); - } - - @Test - void shouldUseRepositoryTypeAsNamespaceForNamesWithSingleElement() throws JAXBException { - updateStep.doUpdate(); - - Optional repository = findByNamespace("git"); - - assertThat(repository) - .get() - .hasFieldOrPropertyWithValue("namespace", "git") - .hasFieldOrPropertyWithValue("name", "simple"); - } - - @Test - void shouldUseDirectoriesForNamespaceAndNameForNamesWithTwoElements() throws JAXBException { - updateStep.doUpdate(); - - Optional repository = findByNamespace("one"); - - assertThat(repository) - .get() - .hasFieldOrPropertyWithValue("namespace", "one") - .hasFieldOrPropertyWithValue("name", "directory"); - } - - @Test - void shouldUseDirectoriesForNamespaceAndConcatenatedNameForNamesWithMoreThanTwoElements() throws JAXBException { - updateStep.doUpdate(); - - Optional repository = findByNamespace("some"); - - assertThat(repository) - .get() - .hasFieldOrPropertyWithValue("namespace", "some") - .hasFieldOrPropertyWithValue("name", "more_directories_than_one"); + .hasFieldOrPropertyWithValue("description", "A repository with two folders."); } @Test void shouldMapPermissions() throws JAXBException { updateStep.doUpdate(); - Optional repository = findByNamespace("git"); + Optional repository = findByNamespace("namespace-454972da-faf9-4437-b682-dc4a4e0aa8eb"); assertThat(repository.get().getPermissions()) .hasSize(3) @@ -179,7 +145,7 @@ class XmlRepositoryV1UpdateStepTest { @Test void shouldUseDirectoryFromStrategy(@TempDirectory.TempDir Path tempDir) throws JAXBException { Path targetDir = tempDir.resolve("someDir"); - MigrationStrategy.Instance strategyMock = injectorMock.getInstance(InlineMigrationStrategy.class); + MigrationStrategy.Instance strategyMock = injectorMock.getInstance(MoveMigrationStrategy.class); when(strategyMock.migrate("454972da-faf9-4437-b682-dc4a4e0aa8eb", "simple", "git")).thenReturn(of(targetDir)); updateStep.doUpdate(); From d9fc1f9aee63ad6b4bc6d1ebce1f7d8f260971a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 11 Jun 2019 13:27:12 +0200 Subject: [PATCH 26/57] Add "change all" button for strategies --- .../templates/repository-migration.mustache | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 9d7e5e0a0b..11d853a2b8 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -58,6 +58,16 @@ Strategy +
Change all: +
+
+ +
+
{{#repositories}} @@ -77,7 +87,7 @@
- {{#strategies}} {{/strategies}} @@ -96,4 +106,15 @@
+ From 00a2be2245338d67a188a579f22e75028eb3e881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 11 Jun 2019 13:33:34 +0200 Subject: [PATCH 27/57] Check for empty (that is: null) permissions --- .../src/main/java/sonia/scm/security/PermissionDescriptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java index 8d95131ee6..3ea7737b4e 100644 --- a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java +++ b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java @@ -100,7 +100,7 @@ public class PermissionDescriptor implements Serializable @Override public int hashCode() { - return value.hashCode(); + return value == null? -1: value.hashCode(); } /** From a456dd5e42a40dc2941c59a0247096c5277d67b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 11 Jun 2019 13:39:14 +0200 Subject: [PATCH 28/57] Check for null values --- .../sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index 2f16dc74f6..ad20b59529 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -181,6 +181,9 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { } private RepositoryPermission[] createPermissions(V1Repository v1Repository) { + if (v1Repository.getPermissions() == null) { + return new RepositoryPermission[0]; + } return v1Repository.getPermissions() .stream() .map(this::createPermission) From 7af5608aeb9927036e7843b374bac9c2765ef923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 11 Jun 2019 14:04:47 +0200 Subject: [PATCH 29/57] Change target version to 2.0.0 --- .../scm/update/repository/MigrateVerbsToPermissionRoles.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java index d197f6fd9c..56227b7b13 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java @@ -117,7 +117,7 @@ public class MigrateVerbsToPermissionRoles extends RepositoryUpdates.RepositoryU @Override public Version getTargetVersion() { - return Version.parse("1"); + return Version.parse("2.0.0"); } @XmlAccessorType(XmlAccessType.FIELD) From a6caa03d86bc794c33c4abee9ba81bcd4a5380c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 11 Jun 2019 14:34:44 +0200 Subject: [PATCH 30/57] Create migration module only if necessary --- .../sonia/scm/update/MigrationWizardContextListener.java | 9 +++++---- .../java/sonia/scm/update/MigrationWizardServlet.java | 7 +------ .../sonia/scm/update/MigrationWizardServletTest.java | 1 - 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java index c98062c900..6929c8b10a 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java @@ -2,21 +2,22 @@ package sonia.scm.update; import com.google.inject.Injector; import com.google.inject.servlet.GuiceServletContextListener; +import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; public class MigrationWizardContextListener extends GuiceServletContextListener { - private final Injector injector; + private final Injector bootstrapInjector; public MigrationWizardContextListener(Injector bootstrapInjector) { - this.injector = bootstrapInjector.createChildInjector(new MigrationWizardModule()); + this.bootstrapInjector = bootstrapInjector; } public boolean wizardNecessary() { - return injector.getInstance(MigrationWizardServlet.class).wizardNecessary(); + return !bootstrapInjector.getInstance(XmlRepositoryV1UpdateStep.class).getRepositoriesWithoutMigrationStrategies().isEmpty(); } @Override protected Injector getInjector() { - return injector; + return bootstrapInjector.createChildInjector(new MigrationWizardModule()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index f26b1afac5..bf89382b1a 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -44,17 +44,13 @@ class MigrationWizardServlet extends HttpServlet { this.migrationStrategyDao = migrationStrategyDao; } - public boolean wizardNecessary() { - return !repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies().isEmpty(); - } - @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { List repositoryLineEntries = getRepositoryLineEntries(); doGet(req, resp, repositoryLineEntries); } - protected void doGet(HttpServletRequest req, HttpServletResponse resp, List repositoryLineEntries) { + private void doGet(HttpServletRequest req, HttpServletResponse resp, List repositoryLineEntries) { HashMap model = new HashMap<>(); model.put("contextPath", req.getContextPath()); @@ -77,7 +73,6 @@ class MigrationWizardServlet extends HttpServlet { String id = repositoryLineEntry.getId(); String namespace = req.getParameter("namespace-" + id); String name = req.getParameter("name-" + id); - String strategy = req.getParameter("strategy-" + id); repositoryLineEntry.setNamespace(namespace); repositoryLineEntry.setName(name); diff --git a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java index e40179eb7f..612e614f6f 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java @@ -157,7 +157,6 @@ class MigrationWizardServletTest { ); doReturn("invalid namespace").when(request).getParameter("namespace-id"); doReturn("invalid name").when(request).getParameter("name-id"); - doReturn("COPY").when(request).getParameter("strategy-id"); servlet.doPost(request, response); From 001dd8eefe49db06f6bec5ef70c0b0e13329d554 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 08:29:28 +0200 Subject: [PATCH 31/57] listen to restart events in every stage, not only development --- scm-core/src/main/java/sonia/scm/boot/RestartEvent.java | 8 +++++--- .../main/java/sonia/scm/boot/BootstrapContextFilter.java | 9 ++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/boot/RestartEvent.java b/scm-core/src/main/java/sonia/scm/boot/RestartEvent.java index 244bc8f03c..9aab8d18ae 100644 --- a/scm-core/src/main/java/sonia/scm/boot/RestartEvent.java +++ b/scm-core/src/main/java/sonia/scm/boot/RestartEvent.java @@ -33,14 +33,16 @@ package sonia.scm.boot; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.Stage; import sonia.scm.event.Event; /** * This event can be used to force a restart of the webapp context. The restart * event is useful during plugin development, because we don't have to restart - * the whole server, to see our changes. The restart event can only be used in - * stage {@link Stage#DEVELOPMENT}. + * the whole server, to see our changes. The restart event could also be used + * to install or upgrade plugins. + * + * But the restart event should be used carefully, because the whole context + * will be restarted and that process could take some time. * * @author Sebastian Sdorra * @since 2.0.0 diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java index aec8e2d653..cf4ec33f1e 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java @@ -37,8 +37,6 @@ import com.github.legman.Subscribe; import com.google.inject.servlet.GuiceFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.SCMContext; -import sonia.scm.Stage; import sonia.scm.event.ScmEventBus; import javax.servlet.FilterConfig; @@ -99,11 +97,8 @@ public class BootstrapContextFilter extends GuiceFilter initGuice(); - if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT) - { - logger.info("register for restart events"); - ScmEventBus.getInstance().register(this); - } + logger.info("register for restart events"); + ScmEventBus.getInstance().register(this); } public void initGuice() throws ServletException { From 24d91a4764857ae389e69e2c473240ff0a624208 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 10:39:49 +0200 Subject: [PATCH 32/57] use mustache template inheritance to reduce duplications between templates --- .../main/resources/templates/layout.mustache | 33 +++ .../repository-migration-restart.mustache | 90 +++----- .../templates/repository-migration.mustache | 192 ++++++++---------- 3 files changed, 154 insertions(+), 161 deletions(-) create mode 100644 scm-webapp/src/main/resources/templates/layout.mustache diff --git a/scm-webapp/src/main/resources/templates/layout.mustache b/scm-webapp/src/main/resources/templates/layout.mustache new file mode 100644 index 0000000000..1132b95a30 --- /dev/null +++ b/scm-webapp/src/main/resources/templates/layout.mustache @@ -0,0 +1,33 @@ + + + + {{$title}}SCM-Manager{{/title}} + + + + +
+ +
+
+
+
+
SCM-Manager
+
+
+
+
+ +
+
+
+

{{$title}}SCM-Manager{{/title}}

+ {{$content}}{{/content}} +
+
+
+ +
+{{$script}}{{/script}} + + diff --git a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache index 1ff8a1baa6..5bda93c7c8 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache @@ -1,36 +1,13 @@ - - - - SCM-Manager Restart - - - - -
-
-
-
-
-
-
-
SCM-Manager
-
-
-
-
-
-
-
-

SCM-Manager will restart to migrate the data.

-
-
-
-

- - +{{ + - - -

-
-
-
-
-
- - - + request.onload = function () { + if (this.readyState == 4 && this.status == 200 && this.response.toString().indexOf("_links") > 0) { + location.href = '{{ contextPath }}'; + } + }; + + request.send(); + }, + 3000 + ); + +{{/script}} + +{{/layout}} diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 11d853a2b8..d322bfa42b 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -1,111 +1,93 @@ - - - - SCM-Manager Migration - - - - -
-
-
-
-
-
-
-
SCM-Manager
+{{< layout}} + +{{$title}}SCM-Manager Migration{{/title}} + +{{$content}} +

You have migrated from SCM-Manager v1 to SCM-Manager v2.

+ +

+ To migrate the existing repositories you have to specify a namespace and a name for each on them + as well as a migration strategy. +

+ +

+ The strategies are the following: +

+ + + {{#strategies}} + + + + + {{/strategies}} +
{{name}}{{description}}
+ +
+ + {{#validationErrorsFound}} +
Please correct the invalid namespaces or names below and try again.
+
+ {{/validationErrorsFound}} + +
+ + + + + + + + {{/repositories}} +
Original nameTypeNew namespace + + New name + + Strategy + +
Change all: +
+
+
- - -
-
-
-

-

SCM-Manager Migration

-

You have migrated from SCM-Manager v1 to SCM-Manager v2.

-

- To migrate the existing repositories you have to specify a namespace and a name for each on them - as well as a migration strategy. -

-

- The strategies are the following: - + + + {{#repositories}} + + + + + + - - - + {{/strategies}} -
+ {{path}} + + {{type}} + + + + + +
+
+
{{name}}{{description}}
-

+ +
-
- {{#validationErrorsFound}} -
Please correct the invalid namespaces or names below and try again.
-
- {{/validationErrorsFound}} - - - - - - - - - - {{#repositories}} - - - - - - - - {{/repositories}} -
Original nameTypeNew namespace - - New name - - Strategy - -
Change all: -
-
- -
-
-
- {{path}} - - {{type}} - - - - - -
-
- -
-
-
- - - - - - - - + +
+ + +{{/content}} + +{{$script}} - +{{/script}} + +{{/ layout}} From c159d209d65b59031626519fd2933d7df69fb634 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 10:51:57 +0200 Subject: [PATCH 33/57] increase compatibility of javascript migration code * replace const with var * replace forEach with a for of loop * use === instead of == --- .../templates/repository-migration-restart.mustache | 4 ++-- .../resources/templates/repository-migration.mustache | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache index 5bda93c7c8..006cc77e2a 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache @@ -48,12 +48,12 @@ {{$script}} From dd61ec8e0a80768cdd520030b39643ad0f046ae0 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 11:00:14 +0200 Subject: [PATCH 34/57] added favicon to migration wizard pages --- .../src/main/java/sonia/scm/update/MigrationWizardModule.java | 2 +- scm-webapp/src/main/resources/templates/layout.mustache | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java index b2c4e842a3..4b357b6d96 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java @@ -20,7 +20,7 @@ class MigrationWizardModule extends ServletModule { LOG.info("= ="); LOG.info("=========================================================="); bind(PushStateDispatcher.class).toInstance((request, response, uri) -> {}); - serve("/images/*", "/styles/*").with(WebResourceServlet.class); + serve("/images/*", "/styles/*", "/favicon.ico").with(WebResourceServlet.class); serve("/*").with(MigrationWizardServlet.class); } } diff --git a/scm-webapp/src/main/resources/templates/layout.mustache b/scm-webapp/src/main/resources/templates/layout.mustache index 1132b95a30..b656984eac 100644 --- a/scm-webapp/src/main/resources/templates/layout.mustache +++ b/scm-webapp/src/main/resources/templates/layout.mustache @@ -3,6 +3,7 @@ {{$title}}SCM-Manager{{/title}} + From 614c61a00c8ed0963b34359250bac19372a32341 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 11:36:15 +0200 Subject: [PATCH 35/57] keep select migration strategy in case of an error --- .../scm/update/MigrationWizardServlet.java | 46 ++++++++++++++++++- .../templates/repository-migration.mustache | 2 +- .../update/MigrationWizardServletTest.java | 37 +++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index bf89382b1a..b0949e82f0 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -4,6 +4,7 @@ import com.github.mustachejava.DefaultMustacheFactory; import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheFactory; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.boot.RestartEvent; @@ -71,9 +72,16 @@ class MigrationWizardServlet extends HttpServlet { boolean validationErrorFound = false; for (RepositoryLineEntry repositoryLineEntry : repositoryLineEntries) { String id = repositoryLineEntry.getId(); + + String strategy = req.getParameter("strategy-" + id); + if (!Strings.isNullOrEmpty(strategy)) { + repositoryLineEntry.setSelectedStrategy(MigrationStrategy.valueOf(strategy)); + } + String namespace = req.getParameter("namespace-" + id); - String name = req.getParameter("name-" + id); repositoryLineEntry.setNamespace(namespace); + + String name = req.getParameter("name-" + id); repositoryLineEntry.setName(name); if (!ValidationUtil.isRepositoryNameValid(namespace)) { @@ -144,6 +152,7 @@ class MigrationWizardServlet extends HttpServlet { private final String id; private final String type; private final String path; + private MigrationStrategy selectedStrategy; private String namespace; private String name; private boolean namespaceValid = true; @@ -153,6 +162,7 @@ class MigrationWizardServlet extends HttpServlet { this.id = repository.getId(); this.type = repository.getType(); this.path = repository.getType() + "/" + repository.getName(); + this.selectedStrategy = MigrationStrategy.COPY; this.namespace = computeNewNamespace(repository); this.name = computeNewName(repository); } @@ -195,6 +205,17 @@ class MigrationWizardServlet extends HttpServlet { return name; } + public MigrationStrategy getSelectedStrategy() { + return selectedStrategy; + } + + public List getStrategies() { + return Arrays.asList(MigrationStrategy.values()) + .stream() + .map(s -> new RepositoryLineMigrationStrategy(s.name(), selectedStrategy == s)) + .collect(Collectors.toList()); + } + public void setNamespace(String namespace) { this.namespace = namespace; } @@ -211,6 +232,10 @@ class MigrationWizardServlet extends HttpServlet { this.nameValid = nameValid; } + public void setSelectedStrategy(MigrationStrategy selectedStrategy) { + this.selectedStrategy = selectedStrategy; + } + public boolean isNamespaceInvalid() { return !namespaceValid; } @@ -219,4 +244,23 @@ class MigrationWizardServlet extends HttpServlet { return !nameValid; } } + + private static class RepositoryLineMigrationStrategy { + + private final String name; + private final boolean selected; + + private RepositoryLineMigrationStrategy(String name, boolean selected) { + this.name = name; + this.selected = selected; + } + + public String getName() { + return name; + } + + public boolean isSelected() { + return selected; + } + } } diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index de4f59a441..8b6c685fc6 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -74,7 +74,7 @@
diff --git a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java index 612e614f6f..fed4a3b6c8 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java @@ -157,6 +157,7 @@ class MigrationWizardServletTest { ); doReturn("invalid namespace").when(request).getParameter("namespace-id"); doReturn("invalid name").when(request).getParameter("name-id"); + doReturn("COPY").when(request).getParameter("strategy-id"); servlet.doPost(request, response); @@ -171,6 +172,42 @@ class MigrationWizardServletTest { .contains(true); } + @Test + void shouldKeepSelectedMigrationStrategy() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "name")) + ); + + doReturn("we need an").when(request).getParameter("namespace-id"); + doReturn("error for this test").when(request).getParameter("name-id"); + doReturn("INLINE").when(request).getParameter("strategy-id"); + + servlet.doPost(request, response); + + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("selectedStrategy") + .contains(MigrationStrategy.INLINE); + } + + @Test + void shouldUseCopyWithoutMigrationStrategy() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "name")) + ); + + doReturn("we need an").when(request).getParameter("namespace-id"); + doReturn("error for this test").when(request).getParameter("name-id"); + doReturn("").when(request).getParameter("strategy-id"); + + servlet.doPost(request, response); + + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("selectedStrategy") + .contains(MigrationStrategy.COPY); + } + @Test void shouldStoreValidMigration() { when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( From fb384cd95de9430886e24d47a3f1dc16db010407 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 11:51:50 +0000 Subject: [PATCH 36/57] Close branch bugfix/check_for_empty_permissions From 249ee68986e9f79bc2a7c6a73584ba281adbd3da Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 14:19:38 +0200 Subject: [PATCH 37/57] use Arrays.stream instead of Arrays.asList(..).stream() --- .../src/main/java/sonia/scm/update/MigrationWizardServlet.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index b0949e82f0..479778852b 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -210,8 +210,7 @@ class MigrationWizardServlet extends HttpServlet { } public List getStrategies() { - return Arrays.asList(MigrationStrategy.values()) - .stream() + return Arrays.stream(MigrationStrategy.values()) .map(s -> new RepositoryLineMigrationStrategy(s.name(), selectedStrategy == s)) .collect(Collectors.toList()); } From c491092c0c82cf277ecaad20912223faef77593f Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 14:20:20 +0200 Subject: [PATCH 38/57] use for..in loop instead of for..of to increase compatibility --- .../main/resources/templates/repository-migration.mustache | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 8b6c685fc6..9c76438667 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -93,8 +93,8 @@ var changeAllSelector = document.getElementById('changeAll'); changeAllSelector.onchange = function () { var strategySelects = document.getElementsByClassName('strategy-select'); - for (var strategySelect of strategySelects) { - strategySelect.value = changeAllSelector.value; + for (var index in strategySelects) { + strategySelects[index].value = changeAllSelector.value; } }; }); From bb38e9e2b93a437d50bd783da3b2405065a791b0 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 12 Jun 2019 15:22:10 +0200 Subject: [PATCH 39/57] add icon ui-component --- .../packages/ui-components/src/Icon.js | 25 +++++++++++++++++++ .../packages/ui-components/src/index.js | 1 + 2 files changed, 26 insertions(+) create mode 100644 scm-ui-components/packages/ui-components/src/Icon.js diff --git a/scm-ui-components/packages/ui-components/src/Icon.js b/scm-ui-components/packages/ui-components/src/Icon.js new file mode 100644 index 0000000000..b3b9a9b4c2 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/Icon.js @@ -0,0 +1,25 @@ +//@flow +import React from "react"; +import classNames from "classnames"; + +type Props = { + title?: string, + name: string +} + +export default class Icon extends React.Component { + + render() { + const { title, name } = this.props; + if(title) { + return ( + + ); + } + return ( + + ); + } + +} + diff --git a/scm-ui-components/packages/ui-components/src/index.js b/scm-ui-components/packages/ui-components/src/index.js index 954b0b0955..82780c94eb 100644 --- a/scm-ui-components/packages/ui-components/src/index.js +++ b/scm-ui-components/packages/ui-components/src/index.js @@ -9,6 +9,7 @@ export { validation, urls, repositories }; export { default as DateFromNow } from "./DateFromNow.js"; export { default as ErrorNotification } from "./ErrorNotification.js"; export { default as ErrorPage } from "./ErrorPage.js"; +export { default as Icon } from "./Icon.js"; export { default as Image } from "./Image.js"; export { default as Loading } from "./Loading.js"; export { default as Logo } from "./Logo.js"; From 467d0fd4aeac9e3d02e220017f4b73a62cd8927d Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 12 Jun 2019 15:23:11 +0200 Subject: [PATCH 40/57] fix vertical alignment of icons in navigation --- .../packages/ui-components/src/navigation/NavLink.js | 3 ++- .../packages/ui-components/src/navigation/SubNavigation.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/navigation/NavLink.js b/scm-ui-components/packages/ui-components/src/navigation/NavLink.js index 98c3138a8f..e1236371a8 100644 --- a/scm-ui-components/packages/ui-components/src/navigation/NavLink.js +++ b/scm-ui-components/packages/ui-components/src/navigation/NavLink.js @@ -1,5 +1,6 @@ //@flow import * as React from "react"; +import classNames from "classnames"; import {Link, Route} from "react-router-dom"; // TODO mostly copy of PrimaryNavigationLink @@ -28,7 +29,7 @@ class NavLink extends React.Component { let showIcon = null; if (icon) { - showIcon = (<>{" "}); + showIcon = (<>{" "}); } return ( diff --git a/scm-ui-components/packages/ui-components/src/navigation/SubNavigation.js b/scm-ui-components/packages/ui-components/src/navigation/SubNavigation.js index 0a6612a173..cdf92d068d 100644 --- a/scm-ui-components/packages/ui-components/src/navigation/SubNavigation.js +++ b/scm-ui-components/packages/ui-components/src/navigation/SubNavigation.js @@ -1,6 +1,7 @@ //@flow import * as React from "react"; import { Link, Route } from "react-router-dom"; +import classNames from "classnames"; type Props = { to: string, @@ -37,7 +38,7 @@ class SubNavigation extends React.Component { return (
  • - {label} + {label} {children}
  • From d8d1513f3cdb8a43f6c4409feb82d60ba0b1d610 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 12 Jun 2019 15:24:32 +0200 Subject: [PATCH 41/57] add is-icon, is-darker class and colored border for card-table --- scm-ui/styles/scm.scss | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index 09a4eb5c27..05bd1c2949 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -219,16 +219,23 @@ ul.is-separated { // card tables .card-table { border-collapse: separate; - border-spacing: 0px 5px; + border-spacing: 0 5px; tr { a { color: #363636; } + &.border-is-green td:first-child { + border-left-color: $green; + } + &.border-is-yellow td:first-child { + border-left-color: $yellow; + } &:hover { td { background-color: whitesmoke; - &:nth-child(4) { + + &.is-darker { background-color: #e1e1e1; } } @@ -238,13 +245,14 @@ ul.is-separated { } } td { - border-bottom: 1px solid whitesmoke; - background-color: #fafafa; padding: 1em 1.25em; + background-color: #fafafa; + border-bottom: 1px solid whitesmoke; + &:first-child { - border-left: 3px solid $mint; + border-left: 3px solid $grey; } - &:nth-child(4) { + &.is-darker { background-color: whitesmoke; } } @@ -318,6 +326,10 @@ form .field:not(.is-grouped) { } } +.is-icon { + color: $grey-light; +} + // label with help-icon compensation .label-icon-spacing { margin-top: 30px; From d50f6e0ec922a306c5c141f487388e6b383a9a08 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 12 Jun 2019 15:29:53 +0200 Subject: [PATCH 42/57] replace checkboxes with icons with hover --- scm-ui/public/locales/de/groups.json | 1 + scm-ui/public/locales/de/users.json | 1 + scm-ui/public/locales/en/groups.json | 1 + scm-ui/public/locales/en/users.json | 1 + scm-ui/src/groups/components/GroupForm.js | 10 ++++++- .../src/groups/components/table/GroupRow.js | 25 +++++++++++----- .../src/groups/components/table/GroupTable.js | 1 - .../containers/SinglePermission.js | 17 +++-------- scm-ui/src/users/components/UserForm.js | 10 ++++++- scm-ui/src/users/components/table/UserRow.js | 30 +++++++++++++------ .../src/users/components/table/UserTable.js | 1 - 11 files changed, 64 insertions(+), 34 deletions(-) diff --git a/scm-ui/public/locales/de/groups.json b/scm-ui/public/locales/de/groups.json index bb8eda1cf2..f1c5f295b1 100644 --- a/scm-ui/public/locales/de/groups.json +++ b/scm-ui/public/locales/de/groups.json @@ -6,6 +6,7 @@ "lastModified": "Zuletzt bearbeitet", "type": "Typ", "external": "Extern", + "internal": "Intern", "members": "Mitglieder" }, "groups": { diff --git a/scm-ui/public/locales/de/users.json b/scm-ui/public/locales/de/users.json index 37416c1e4f..25b6858c8b 100644 --- a/scm-ui/public/locales/de/users.json +++ b/scm-ui/public/locales/de/users.json @@ -5,6 +5,7 @@ "mail": "E-Mail", "password": "Passwort", "active": "Aktiv", + "inactive": "Inaktiv", "type": "Typ", "creationDate": "Erstellt", "lastModified": "Zuletzt bearbeitet" diff --git a/scm-ui/public/locales/en/groups.json b/scm-ui/public/locales/en/groups.json index aff350a458..f14347ed30 100644 --- a/scm-ui/public/locales/en/groups.json +++ b/scm-ui/public/locales/en/groups.json @@ -6,6 +6,7 @@ "lastModified": "Last Modified", "type": "Type", "external": "External", + "internal": "Internal", "members": "Members" }, "groups": { diff --git a/scm-ui/public/locales/en/users.json b/scm-ui/public/locales/en/users.json index d188f6221b..b492923a3f 100644 --- a/scm-ui/public/locales/en/users.json +++ b/scm-ui/public/locales/en/users.json @@ -5,6 +5,7 @@ "mail": "E-Mail", "password": "Password", "active": "Active", + "inactive": "Inactive", "type": "Type", "creationDate": "Creation Date", "lastModified": "Last Modified" diff --git a/scm-ui/src/groups/components/GroupForm.js b/scm-ui/src/groups/components/GroupForm.js index baa68de8ef..dae1229f10 100644 --- a/scm-ui/src/groups/components/GroupForm.js +++ b/scm-ui/src/groups/components/GroupForm.js @@ -9,6 +9,7 @@ import { InputField, SubmitButton, Textarea, + Icon, Checkbox } from "@scm-manager/ui-components"; import type { Group, SelectValue } from "@scm-manager/ui-types"; @@ -113,9 +114,16 @@ class GroupForm extends React.Component { if (this.isExistingGroup()) { return null; } + + const iconType = group && group.external ? ( + + ) : ( + + ); + return ( {iconType} {t("group.external")}} checked={group.external} helpText={t("groupForm.help.externalHelpText")} onChange={this.handleExternalChange} diff --git a/scm-ui/src/groups/components/table/GroupRow.js b/scm-ui/src/groups/components/table/GroupRow.js index 20bc90279e..509fe09ce4 100644 --- a/scm-ui/src/groups/components/table/GroupRow.js +++ b/scm-ui/src/groups/components/table/GroupRow.js @@ -1,29 +1,38 @@ // @flow import React from "react"; +import { translate } from "react-i18next"; import { Link } from "react-router-dom"; import type { Group } from "@scm-manager/ui-types"; -import { Checkbox } from "@scm-manager/ui-components"; +import { Icon } from "@scm-manager/ui-components"; type Props = { - group: Group + group: Group, + + // context props + t: string => string }; -export default class GroupRow extends React.Component { +class GroupRow extends React.Component { renderLink(to: string, label: string) { return {label}; } render() { - const { group } = this.props; + const { group, t } = this.props; const to = `/group/${group.name}`; + const iconType = group.external ? ( + + ) : ( + + ); + return ( - {this.renderLink(to, group.name)} + {iconType} {this.renderLink(to, group.name)} {group.description} - - - ); } } + +export default translate("groups")(GroupRow); diff --git a/scm-ui/src/groups/components/table/GroupTable.js b/scm-ui/src/groups/components/table/GroupTable.js index 85bcb813fc..c7b59b40a7 100644 --- a/scm-ui/src/groups/components/table/GroupTable.js +++ b/scm-ui/src/groups/components/table/GroupTable.js @@ -18,7 +18,6 @@ class GroupTable extends React.Component { {t("group.name")} {t("group.description")} - {t("group.external")} diff --git a/scm-ui/src/repos/permissions/containers/SinglePermission.js b/scm-ui/src/repos/permissions/containers/SinglePermission.js index bdb96de0bd..37f3436af3 100644 --- a/scm-ui/src/repos/permissions/containers/SinglePermission.js +++ b/scm-ui/src/repos/permissions/containers/SinglePermission.js @@ -11,7 +11,7 @@ import { } from "../modules/permissions"; import { connect } from "react-redux"; import type { History } from "history"; -import { Button } from "@scm-manager/ui-components"; +import { Button, Icon } from "@scm-manager/ui-components"; import DeletePermissionButton from "../components/buttons/DeletePermissionButton"; import RoleSelector from "../components/RoleSelector"; import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog"; @@ -49,9 +49,6 @@ type State = { }; const styles = { - iconColor: { - color: "#9a9a9a" - }, centerMiddle: { display: "table-cell", verticalAlign: "middle !important" @@ -148,15 +145,9 @@ class SinglePermission extends React.Component { const iconType = permission && permission.groupPermission ? ( - + ) : ( - + ); return ( @@ -171,7 +162,7 @@ class SinglePermission extends React.Component { action={this.handleDetailedPermissionsPressed} /> - + { // edit existing user subtitle = ; } + + const iconType = user && user.active ? ( + + ) : ( + + ); + return ( <> {subtitle} @@ -167,7 +175,7 @@ class UserForm extends React.Component {
    {passwordChangeField} {iconType} {t("user.active")}} onChange={this.handleActiveChange} checked={user ? user.active : false} helpText={t("help.activeHelpText")} diff --git a/scm-ui/src/users/components/table/UserRow.js b/scm-ui/src/users/components/table/UserRow.js index 9cb55295fb..03c7f1ebaa 100644 --- a/scm-ui/src/users/components/table/UserRow.js +++ b/scm-ui/src/users/components/table/UserRow.js @@ -1,31 +1,43 @@ // @flow import React from "react"; +import { translate } from "react-i18next"; import { Link } from "react-router-dom"; import type { User } from "@scm-manager/ui-types"; +import { Icon } from "@scm-manager/ui-components"; type Props = { - user: User + user: User, + + // context props + t: string => string }; -export default class UserRow extends React.Component { +class UserRow extends React.Component { renderLink(to: string, label: string) { return {label}; } render() { - const { user } = this.props; + const { user, t } = this.props; const to = `/user/${user.name}`; + const iconType = user.active ? ( + + ) : ( + + ); + return ( - - {this.renderLink(to, user.name)} - {this.renderLink(to, user.displayName)} + + {iconType} {this.renderLink(to, user.name)} + + {this.renderLink(to, user.displayName)} + {user.mail} - - - ); } } + +export default translate("users")(UserRow); diff --git a/scm-ui/src/users/components/table/UserTable.js b/scm-ui/src/users/components/table/UserTable.js index 8c012f11f4..ab29dca9e7 100644 --- a/scm-ui/src/users/components/table/UserTable.js +++ b/scm-ui/src/users/components/table/UserTable.js @@ -19,7 +19,6 @@ class UserTable extends React.Component { {t("user.name")} {t("user.displayName")} {t("user.mail")} - {t("user.active")} From 34b1047d39e461ac34026c68c33595a63f441911 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 13:40:00 +0000 Subject: [PATCH 43/57] Close branch feature/migration_servlet From 5f435a524e4930a894ef9cb823dc8a68a5a622f8 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 12 Jun 2019 16:27:16 +0200 Subject: [PATCH 44/57] clarifiy checkbox --- .../packages/ui-components/src/forms/Radio.js | 25 +++++++++---------- scm-ui/src/groups/components/GroupForm.js | 6 ++--- scm-ui/src/users/components/UserForm.js | 20 ++++++++++----- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/forms/Radio.js b/scm-ui-components/packages/ui-components/src/forms/Radio.js index 6460e67070..7e61b25ec0 100644 --- a/scm-ui-components/packages/ui-components/src/forms/Radio.js +++ b/scm-ui-components/packages/ui-components/src/forms/Radio.js @@ -22,19 +22,18 @@ class Radio extends React.Component { render() { return ( - - + ); } } diff --git a/scm-ui/src/groups/components/GroupForm.js b/scm-ui/src/groups/components/GroupForm.js index dae1229f10..07e9085af7 100644 --- a/scm-ui/src/groups/components/GroupForm.js +++ b/scm-ui/src/groups/components/GroupForm.js @@ -116,14 +116,14 @@ class GroupForm extends React.Component { } const iconType = group && group.external ? ( - + <>{t("group.external")} ) : ( - + <>{t("group.internal")} ); return ( {iconType} {t("group.external")}} + label={iconType} checked={group.external} helpText={t("groupForm.help.externalHelpText")} onChange={this.handleExternalChange} diff --git a/scm-ui/src/users/components/UserForm.js b/scm-ui/src/users/components/UserForm.js index 48314bef47..a6de2e7bca 100644 --- a/scm-ui/src/users/components/UserForm.js +++ b/scm-ui/src/users/components/UserForm.js @@ -17,6 +17,8 @@ type Props = { submitForm: User => void, user?: User, loading?: boolean, + + // context props t: string => string }; @@ -138,11 +140,17 @@ class UserForm extends React.Component { subtitle = ; } - const iconType = user && user.active ? ( - - ) : ( - - ); + const iconType = + user && user.active ? ( + <> + {t("user.active")} + + ) : ( + <> + {t("user.inactive")}{" "} + + + ); return ( <> @@ -175,7 +183,7 @@ class UserForm extends React.Component {
    {passwordChangeField} {iconType} {t("user.active")}} + label={iconType} onChange={this.handleActiveChange} checked={user ? user.active : false} helpText={t("help.activeHelpText")} From 2727679f6e7cc7bd8bec4bc94aee2030b68f79ad Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 12 Jun 2019 16:35:08 +0200 Subject: [PATCH 45/57] rename local const --- scm-ui/src/groups/components/GroupForm.js | 4 ++-- scm-ui/src/users/components/UserForm.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scm-ui/src/groups/components/GroupForm.js b/scm-ui/src/groups/components/GroupForm.js index 07e9085af7..637be3faff 100644 --- a/scm-ui/src/groups/components/GroupForm.js +++ b/scm-ui/src/groups/components/GroupForm.js @@ -115,7 +115,7 @@ class GroupForm extends React.Component { return null; } - const iconType = group && group.external ? ( + const label = group && group.external ? ( <>{t("group.external")} ) : ( <>{t("group.internal")} @@ -123,7 +123,7 @@ class GroupForm extends React.Component { return ( { subtitle = ; } - const iconType = + const label = user && user.active ? ( <> {t("user.active")} @@ -183,7 +183,7 @@ class UserForm extends React.Component {
    {passwordChangeField} Date: Wed, 12 Jun 2019 17:04:32 +0200 Subject: [PATCH 46/57] make PluginLoader dependency of MustacheTemplateEngine optional --- .../scm/template/MustacheTemplateEngine.java | 36 +++++++++++-------- .../scm/template/ServletMustacheFactory.java | 23 +++++------- .../template/MustacheTemplateEngineTest.java | 34 ++++++++++++++---- 3 files changed, 57 insertions(+), 36 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/template/MustacheTemplateEngine.java b/scm-webapp/src/main/java/sonia/scm/template/MustacheTemplateEngine.java index 06bd8b0f62..170c0cd641 100644 --- a/scm-webapp/src/main/java/sonia/scm/template/MustacheTemplateEngine.java +++ b/scm-webapp/src/main/java/sonia/scm/template/MustacheTemplateEngine.java @@ -37,27 +37,22 @@ package sonia.scm.template; import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheException; - import com.google.common.base.Throwables; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.common.util.concurrent.UncheckedExecutionException; import com.google.inject.Inject; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.Default; import sonia.scm.plugin.PluginLoader; -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.ServletContext; import java.io.IOException; import java.io.Reader; - import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; -import javax.servlet.ServletContext; +//~--- JDK imports ------------------------------------------------------------ /** * @@ -67,6 +62,14 @@ import javax.servlet.ServletContext; public class MustacheTemplateEngine implements TemplateEngine { + /** + * Used to implement optional injection for the PluginLoader. + * @see Optional Injection + */ + static class PluginLoaderHolder { + @Inject(optional = true) PluginLoader pluginLoader; + } + /** Field description */ public static final TemplateType TYPE = new TemplateType("mustache", "Mustache", "mustache"); @@ -87,13 +90,12 @@ public class MustacheTemplateEngine implements TemplateEngine * * * @param context - * @param pluginLoader + * @param pluginLoaderHolder */ @Inject - public MustacheTemplateEngine(@Default ServletContext context, - PluginLoader pluginLoader) + public MustacheTemplateEngine(@Default ServletContext context, PluginLoaderHolder pluginLoaderHolder) { - factory = new ServletMustacheFactory(context, pluginLoader); + factory = new ServletMustacheFactory(context, createClassLoader(pluginLoaderHolder.pluginLoader)); ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(THREAD_NAME).build(); @@ -101,6 +103,13 @@ public class MustacheTemplateEngine implements TemplateEngine factory.setExecutorService(Executors.newCachedThreadPool(threadFactory)); } + private ClassLoader createClassLoader(PluginLoader pluginLoader) { + if (pluginLoader == null) { + return Thread.currentThread().getContextClassLoader(); + } + return pluginLoader.getUberClassLoader(); + } + //~--- get methods ---------------------------------------------------------- /** @@ -112,12 +121,9 @@ public class MustacheTemplateEngine implements TemplateEngine * * @return * - * @throws IOException */ @Override - public Template getTemplate(String templateIdentifier, Reader reader) - throws IOException - { + public Template getTemplate(String templateIdentifier, Reader reader) { if (logger.isTraceEnabled()) { logger.trace("try to create mustache template from reader with id {}", diff --git a/scm-webapp/src/main/java/sonia/scm/template/ServletMustacheFactory.java b/scm-webapp/src/main/java/sonia/scm/template/ServletMustacheFactory.java index c6e5b82441..24c8ff21c9 100644 --- a/scm-webapp/src/main/java/sonia/scm/template/ServletMustacheFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/template/ServletMustacheFactory.java @@ -36,22 +36,17 @@ package sonia.scm.template; //~--- non-JDK imports -------------------------------------------------------- import com.github.mustachejava.DefaultMustacheFactory; - import com.google.common.base.Charsets; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.plugin.PluginLoader; - -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.ServletContext; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import javax.servlet.ServletContext; +//~--- JDK imports ------------------------------------------------------------ /** * @@ -73,13 +68,12 @@ public class ServletMustacheFactory extends DefaultMustacheFactory * * * @param servletContext - * @param pluginLoader + * @param classLoader */ - public ServletMustacheFactory(ServletContext servletContext, - PluginLoader pluginLoader) + public ServletMustacheFactory(ServletContext servletContext, ClassLoader classLoader) { this.servletContext = servletContext; - this.pluginLoader = pluginLoader; + this.classLoader = classLoader; } //~--- get methods ---------------------------------------------------------- @@ -116,7 +110,7 @@ public class ServletMustacheFactory extends DefaultMustacheFactory resourceName = resourceName.substring(1); } - is = pluginLoader.getUberClassLoader().getResourceAsStream(resourceName); + is = classLoader.getResourceAsStream(resourceName); } if (is != null) @@ -144,9 +138,8 @@ public class ServletMustacheFactory extends DefaultMustacheFactory //~--- fields --------------------------------------------------------------- - /** Field description */ - private final PluginLoader pluginLoader; - /** Field description */ private ServletContext servletContext; + + private ClassLoader classLoader; } diff --git a/scm-webapp/src/test/java/sonia/scm/template/MustacheTemplateEngineTest.java b/scm-webapp/src/test/java/sonia/scm/template/MustacheTemplateEngineTest.java index be0c33f098..131db6e44d 100644 --- a/scm-webapp/src/test/java/sonia/scm/template/MustacheTemplateEngineTest.java +++ b/scm-webapp/src/test/java/sonia/scm/template/MustacheTemplateEngineTest.java @@ -35,16 +35,21 @@ package sonia.scm.template; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.collect.ImmutableMap; +import org.assertj.core.api.Assertions; +import org.junit.Test; import sonia.scm.plugin.PluginLoader; -import static org.mockito.Mockito.*; +import javax.servlet.ServletContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ -import java.io.InputStream; - -import javax.servlet.ServletContext; - /** * * @author Sebastian Sdorra @@ -68,7 +73,10 @@ public class MustacheTemplateEngineTest extends TemplateEngineTestBase when(loader.getUberClassLoader()).thenReturn( Thread.currentThread().getContextClassLoader()); - return new MustacheTemplateEngine(context, loader); + MustacheTemplateEngine.PluginLoaderHolder holder = new MustacheTemplateEngine.PluginLoaderHolder(); + holder.pluginLoader = loader; + + return new MustacheTemplateEngine(context, holder); } //~--- get methods ---------------------------------------------------------- @@ -116,4 +124,18 @@ public class MustacheTemplateEngineTest extends TemplateEngineTestBase return MustacheTemplateEngineTest.class.getResourceAsStream( "/sonia/scm/template/".concat(resource).concat(".mustache")); } + + @Test + public void testCreateEngineWithoutPluginLoader() throws IOException { + ServletContext context = mock(ServletContext.class); + MustacheTemplateEngine.PluginLoaderHolder holder = new MustacheTemplateEngine.PluginLoaderHolder(); + MustacheTemplateEngine engine = new MustacheTemplateEngine(context, holder); + + Template template = engine.getTemplate(getTemplateResource()); + + StringWriter writer = new StringWriter(); + template.execute(writer, ImmutableMap.of("name", "World")); + + Assertions.assertThat(writer.toString()).isEqualTo("Hello World!"); + } } From df3e4395b0227d97d0b81a33c2b33441979e030e Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 18:23:21 +0200 Subject: [PATCH 47/57] remove unused template from v1 --- .../main/resources/templates/error.mustache | 14 ++ .../templates/repository-root.mustache | 102 ------------ .../main/resources/templates/support.mustache | 150 ------------------ 3 files changed, 14 insertions(+), 252 deletions(-) create mode 100644 scm-webapp/src/main/resources/templates/error.mustache delete mode 100644 scm-webapp/src/main/resources/templates/repository-root.mustache delete mode 100644 scm-webapp/src/main/resources/templates/support.mustache diff --git a/scm-webapp/src/main/resources/templates/error.mustache b/scm-webapp/src/main/resources/templates/error.mustache new file mode 100644 index 0000000000..ade00d002b --- /dev/null +++ b/scm-webapp/src/main/resources/templates/error.mustache @@ -0,0 +1,14 @@ +{{< layout}} + + {{$title}}SCM-Manager Error{{/title}} + + {{$content}} +

    There is an error occurred during SCM-Manager startup.

    + +
    +
    +        {{ error }}
    +      
    +
    + {{/content}} +{{/ layout}} diff --git a/scm-webapp/src/main/resources/templates/repository-root.mustache b/scm-webapp/src/main/resources/templates/repository-root.mustache deleted file mode 100644 index 7e5cd5951e..0000000000 --- a/scm-webapp/src/main/resources/templates/repository-root.mustache +++ /dev/null @@ -1,102 +0,0 @@ - - - - - SCM-Manager support information - - - - -

    SCM-Manager Repositories

    - -
      - {{#repositories}} -
    • - {{name}} -
    • - {{/repositories}} -
    - - - diff --git a/scm-webapp/src/main/resources/templates/support.mustache b/scm-webapp/src/main/resources/templates/support.mustache deleted file mode 100644 index 822ac5e9d5..0000000000 --- a/scm-webapp/src/main/resources/templates/support.mustache +++ /dev/null @@ -1,150 +0,0 @@ - - - - - SCM-Manager support information - - - - -

    SCM-Manager support information

    - -

    Information for SCM-Manager support.

    - -

    Version

    - -
      -
    • Version: {{version.version}}
    • -
    • Stage: {{version.stage}}
    • -
    • StoreFactory: {{version.storeFactory}}
    • -
    - -

    Configuration

    - -
      -
    • Anonymous Access Enabled: {{configuration.anonymousAccessEnabled}}
    • -
    • Enable Proxy: {{configuration.enableProxy}}
    • -
    • Force Base Url: {{configuration.forceBaseUrl}}
    • -
    • Disable Grouping Grid: {{configuration.disableGroupingGrid}}
    • -
    • Enable Repository Archive: {{configuration.enableRepositoryArchive}}
    • -
    - -

    Installed Plugins

    - -
      - {{#pluginManager.installed}} -
    • {{id}}
    • - {{/pluginManager.installed}} -
    - -

    Runtime

    - -
      -
    • Free Memory: {{runtime.freeMemory}}
    • -
    • Total Memory: {{runtime.totalMemory}}
    • -
    • Max Memory: {{runtime.maxMemory}}
    • -
    • Available Processors: {{runtime.availableProcessors}}
    • -
    - -

    System

    - -
      -
    • OS: {{system.os}}
    • -
    • Architecture: {{system.arch}}
    • -
    • ServletContainer: {{system.container}}
    • -
    • Java: {{system.java}}
    • -
    • Local: {{system.locale}}
    • -
    • TimeZone: {{system.timeZone}}
    • -
    - -

    Repository Handlers

    - -
      - {{#repositoryHandlers}} -
    • {{type.displayName}}/{{type.name}} ({{versionInformation}})
    • - {{/repositoryHandlers}} -
    - - - From bc7402053a6c2fb18363f3b15f92eec0032ca72c Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 18:25:38 +0200 Subject: [PATCH 48/57] remove outdated error module --- .../java/sonia/scm/ScmContextListener.java | 35 ++-- .../main/java/sonia/scm/ScmErrorModule.java | 74 ------- .../java/sonia/scm/template/ErrorServlet.java | 191 ------------------ 3 files changed, 12 insertions(+), 288 deletions(-) delete mode 100644 scm-webapp/src/main/java/sonia/scm/ScmErrorModule.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/template/ErrorServlet.java diff --git a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java index 874052442d..cc58d34ef3 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java @@ -75,7 +75,7 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList * the logger for ScmContextListener */ private static final Logger LOG = LoggerFactory.getLogger(ScmContextListener.class); - + private final ClassLoader parent; private final Set plugins; private Injector injector; @@ -101,23 +101,16 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList super.contextInitialized(servletContextEvent); afterInjectorCreation(servletContextEvent); } - + private void beforeInjectorCreation() { } private boolean hasStartupErrors() { return SCMContext.getContext().getStartupError() != null; } - + @Override protected List getModules(ServletContext context) { - if (hasStartupErrors()) { - return getErrorModules(); - } - return getDefaultModules(context); - } - - private List getDefaultModules(ServletContext context) { DefaultPluginLoader pluginLoader = new DefaultPluginLoader(context, parent, plugins); ClassOverrides overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader()); @@ -132,15 +125,15 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList ); appendModules(pluginLoader.getExtensionProcessor(), moduleList); moduleList.addAll(overrides.getModules()); - + if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT){ moduleList.add(new DebugModule()); } moduleList.add(new MapperModule()); - return moduleList; + return moduleList; } - + private void appendModules(ExtensionProcessor ep, List moduleList) { for (Class module : ep.byExtensionPoint(Module.class)) { try { @@ -151,31 +144,27 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList } } } - - private List getErrorModules() { - return Collections.singletonList(new ScmErrorModule()); - } @Override protected void withInjector(Injector injector) { this.injector = injector; } - + private void afterInjectorCreation(ServletContextEvent event) { if (injector != null && !hasStartupErrors()) { bindEagerSingletons(); initializeServletContextListeners(event); - } + } } - + private void bindEagerSingletons() { injector.getInstance(EagerSingletonModule.class).initialize(injector); } - + private void initializeServletContextListeners(ServletContextEvent event) { injector.getInstance(ServletContextListenerHolder.class).contextInitialized(event); } - + @Override public void contextDestroyed(ServletContextEvent servletContextEvent) { @@ -198,7 +187,7 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList } } } - + private void closeCloseables() { // close Scheduler IOUtil.close(injector.getInstance(Scheduler.class)); diff --git a/scm-webapp/src/main/java/sonia/scm/ScmErrorModule.java b/scm-webapp/src/main/java/sonia/scm/ScmErrorModule.java deleted file mode 100644 index 56ae8527f2..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/ScmErrorModule.java +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.multibindings.Multibinder; -import com.google.inject.servlet.ServletModule; - -import sonia.scm.template.ErrorServlet; -import sonia.scm.template.MustacheTemplateEngine; -import sonia.scm.template.TemplateEngine; -import sonia.scm.template.TemplateEngineFactory; - -/** - * - * @author Sebastian Sdorra - */ -public class ScmErrorModule extends ServletModule -{ - - /** - * Method description - * - */ - @Override - protected void configureServlets() - { - SCMContextProvider context = SCMContext.getContext(); - - bind(SCMContextProvider.class).toInstance(context); - - Multibinder engineBinder = - Multibinder.newSetBinder(binder(), TemplateEngine.class); - - engineBinder.addBinding().to(MustacheTemplateEngine.class); - bind(TemplateEngine.class).annotatedWith(Default.class).to( - MustacheTemplateEngine.class); - bind(TemplateEngineFactory.class); - - serve(ScmServletModule.PATTERN_ALL).with(ErrorServlet.class); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/template/ErrorServlet.java b/scm-webapp/src/main/java/sonia/scm/template/ErrorServlet.java deleted file mode 100644 index 4d62b823a1..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/template/ErrorServlet.java +++ /dev/null @@ -1,191 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.template; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Throwables; -import com.google.inject.Inject; -import com.google.inject.Singleton; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.SCMContextProvider; -import sonia.scm.util.IOUtil; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; -import java.io.PrintWriter; - -import java.util.HashMap; -import java.util.Map; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * - * @author Sebastian Sdorra - */ -@Singleton -public class ErrorServlet extends HttpServlet -{ - - /** Field description */ - private static final String TEMPALTE = "/error.mustache"; - - /** Field description */ - private static final long serialVersionUID = -3289076078469757874L; - - /** - * the logger for ErrorServlet - */ - private static final Logger logger = - LoggerFactory.getLogger(ErrorServlet.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param context - * @param templateEngineFactory - */ - @Inject - public ErrorServlet(SCMContextProvider context, - TemplateEngineFactory templateEngineFactory) - { - this.context = context; - this.templateEngineFactory = templateEngineFactory; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param response - * - * @throws IOException - * @throws ServletException - */ - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException - { - processRequest(request, response); - } - - /** - * Method description - * - * - * @param request - * @param response - * - * @throws IOException - * @throws ServletException - */ - @Override - protected void doPost(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException - { - processRequest(request, response); - } - - /** - * Method description - * - * - * @param request - * @param response - * - * @throws IOException - * @throws ServletException - */ - private void processRequest(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException - { - PrintWriter writer = null; - - try - { - writer = response.getWriter(); - - Map env = new HashMap(); - String error = Util.EMPTY_STRING; - - if (context.getStartupError() != null) - { - error = Throwables.getStackTraceAsString(context.getStartupError()); - } - - env.put("error", error); - - TemplateEngine engine = templateEngineFactory.getDefaultEngine(); - Template template = engine.getTemplate(TEMPALTE); - - if (template != null) - { - template.execute(writer, env); - } - else if (logger.isWarnEnabled()) - { - logger.warn("could not find template {}", TEMPALTE); - } - } - finally - { - IOUtil.close(writer); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final SCMContextProvider context; - - /** Field description */ - private final TemplateEngineFactory templateEngineFactory; -} From 5c7ae749c213ab713fc89a2baded0b5c452af72e Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 18:26:58 +0200 Subject: [PATCH 49/57] create new error module, which displays errors before migration --- .../scm/boot/BootstrapContextListener.java | 18 ++- .../main/java/sonia/scm/boot/SingleView.java | 109 ++++++++++++++++++ .../sonia/scm/boot/SingleViewServlet.java | 63 ++++++++++ .../sonia/scm/boot/StaticResourceServlet.java | 39 +++++++ .../src/main/java/sonia/scm/boot/View.java | 20 ++++ .../java/sonia/scm/boot/ViewController.java | 11 ++ .../sonia/scm/boot/SingleViewServletTest.java | 90 +++++++++++++++ .../java/sonia/scm/boot/SingleViewTest.java | 99 ++++++++++++++++ .../scm/boot/StaticResourceServletTest.java | 61 ++++++++++ .../resources/sonia/scm/boot/resource.txt | 1 + 10 files changed, 506 insertions(+), 5 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/boot/SingleView.java create mode 100644 scm-webapp/src/main/java/sonia/scm/boot/SingleViewServlet.java create mode 100644 scm-webapp/src/main/java/sonia/scm/boot/StaticResourceServlet.java create mode 100644 scm-webapp/src/main/java/sonia/scm/boot/View.java create mode 100644 scm-webapp/src/main/java/sonia/scm/boot/ViewController.java create mode 100644 scm-webapp/src/test/java/sonia/scm/boot/SingleViewServletTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/boot/StaticResourceServletTest.java create mode 100644 scm-webapp/src/test/resources/sonia/scm/boot/resource.txt diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index 76aad46b77..844a04c79f 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -69,7 +69,6 @@ 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.Closeable; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; @@ -126,9 +125,7 @@ public class BootstrapContextListener implements ServletContextListener { public void contextInitialized(ServletContextEvent sce) { context = sce.getServletContext(); - File pluginDirectory = getPluginDirectory(); - - createContextListener(pluginDirectory); + createContextListener(); contextListener.contextInitialized(sce); @@ -140,12 +137,23 @@ public class BootstrapContextListener implements ServletContextListener { } } - private void createContextListener(File pluginDirectory) { + private void createContextListener() { + Throwable startupError = SCMContext.getContext().getStartupError(); + if (startupError != null) { + contextListener = SingleView.error(startupError); + } else { + createMigrationOrNormalContextListener(); + } + } + + private void createMigrationOrNormalContextListener() { ClassLoader cl; Set plugins; PluginLoader pluginLoader; try { + File pluginDirectory = getPluginDirectory(); + renameOldPluginsFolder(pluginDirectory); if (!isCorePluginExtractionDisabled()) { diff --git a/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java b/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java new file mode 100644 index 0000000000..f1c57ce9c6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java @@ -0,0 +1,109 @@ +package sonia.scm.boot; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.multibindings.Multibinder; +import com.google.inject.servlet.GuiceServletContextListener; +import com.google.inject.servlet.ServletModule; +import sonia.scm.Default; +import sonia.scm.SCMContext; +import sonia.scm.SCMContextProvider; +import sonia.scm.template.MustacheTemplateEngine; +import sonia.scm.template.TemplateEngine; +import sonia.scm.template.TemplateEngineFactory; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextListener; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +final class SingleView { + + private SingleView() { + } + + static ServletContextListener error(Throwable throwable) { + String error = Throwables.getStackTraceAsString(throwable); + + ViewController controller = new SimpleViewController("/templates/error.mustache", request -> { + Object model = ImmutableMap.of( + "contextPath", request.getContextPath(), + "error", error + ); + return new View(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, model); + }); + return new SingleViewContextListener(controller); + } + + private static class SingleViewContextListener extends GuiceServletContextListener { + + private final ViewController controller; + + private SingleViewContextListener(ViewController controller) { + this.controller = controller; + } + + @Override + protected Injector getInjector() { + return Guice.createInjector(new SingleViewModule(controller)); + } + } + + private static class SingleViewModule extends ServletModule { + + private final ViewController viewController; + + private SingleViewModule(ViewController viewController) { + this.viewController = viewController; + } + + @Override + protected void configureServlets() { + SCMContextProvider context = SCMContext.getContext(); + + bind(SCMContextProvider.class).toInstance(context); + bind(ViewController.class).toInstance(viewController); + + Multibinder engineBinder = + Multibinder.newSetBinder(binder(), TemplateEngine.class); + + engineBinder.addBinding().to(MustacheTemplateEngine.class); + bind(TemplateEngine.class).annotatedWith(Default.class).to( + MustacheTemplateEngine.class); + bind(TemplateEngineFactory.class); + + bind(ServletContext.class).annotatedWith(Default.class).toInstance(getServletContext()); + + serve("/images/*", "/styles/*", "/favicon.ico").with(StaticResourceServlet.class); + serve("/*").with(SingleViewServlet.class); + } + } + + private static class SimpleViewController implements ViewController { + + private final String template; + private final SimpleViewFactory viewFactory; + + private SimpleViewController(String template, SimpleViewFactory viewFactory) { + this.template = template; + this.viewFactory = viewFactory; + } + + @Override + public String getTemplate() { + return template; + } + + @Override + public View createView(HttpServletRequest request) { + return viewFactory.create(request); + } + } + + @FunctionalInterface + interface SimpleViewFactory { + View create(HttpServletRequest request); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/SingleViewServlet.java b/scm-webapp/src/main/java/sonia/scm/boot/SingleViewServlet.java new file mode 100644 index 0000000000..3621105976 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/SingleViewServlet.java @@ -0,0 +1,63 @@ +package sonia.scm.boot; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.template.Template; +import sonia.scm.template.TemplateEngine; +import sonia.scm.template.TemplateEngineFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +@Singleton +public class SingleViewServlet extends HttpServlet { + + private static final Logger LOG = LoggerFactory.getLogger(SingleViewServlet.class); + + private final Template template; + private final ViewController controller; + + @Inject + public SingleViewServlet(TemplateEngineFactory templateEngineFactory, ViewController controller) { + template = createTemplate(templateEngineFactory, controller.getTemplate()); + this.controller = controller; + } + + private Template createTemplate(TemplateEngineFactory templateEngineFactory, String template) { + TemplateEngine engine = templateEngineFactory.getEngineByExtension(template); + try { + return engine.getTemplate(template); + } catch (IOException e) { + throw new IllegalStateException("failed to parse template: " + template, e); + } + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + process(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + process(req, resp); + } + + private void process(HttpServletRequest request, HttpServletResponse response) { + View view = controller.createView(request); + + response.setStatus(view.getStatusCode()); + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + + try (PrintWriter writer = response.getWriter()) { + template.execute(writer, view.getModel()); + } catch (IOException ex) { + LOG.error("failed to write view", ex); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/StaticResourceServlet.java b/scm-webapp/src/main/java/sonia/scm/boot/StaticResourceServlet.java new file mode 100644 index 0000000000..b44fbb64bc --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/StaticResourceServlet.java @@ -0,0 +1,39 @@ +package sonia.scm.boot; + +import com.github.sdorra.webresources.CacheControl; +import com.github.sdorra.webresources.WebResourceSender; +import com.google.common.annotations.VisibleForTesting; +import sonia.scm.util.HttpUtil; + +import javax.inject.Singleton; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +@Singleton +public class StaticResourceServlet extends HttpServlet { + + private final WebResourceSender sender = WebResourceSender.create() + .withGZIP() + .withGZIPMinLength(512) + .withBufferSize(16384) + .withCacheControl(CacheControl.create().noCache()); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + URL resource = createResourceUrlFromRequest(request); + if (resource != null) { + sender.resource(resource).get(request, response); + } else { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } + + private URL createResourceUrlFromRequest(HttpServletRequest request) throws MalformedURLException { + String uri = HttpUtil.getStrippedURI(request); + return request.getServletContext().getResource(uri); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/View.java b/scm-webapp/src/main/java/sonia/scm/boot/View.java new file mode 100644 index 0000000000..6e1f93bd3f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/View.java @@ -0,0 +1,20 @@ +package sonia.scm.boot; + +class View { + + private final int statusCode; + private final Object model; + + View(int statusCode, Object model) { + this.statusCode = statusCode; + this.model = model; + } + + int getStatusCode() { + return statusCode; + } + + Object getModel() { + return model; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/ViewController.java b/scm-webapp/src/main/java/sonia/scm/boot/ViewController.java new file mode 100644 index 0000000000..26f463f9c2 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/ViewController.java @@ -0,0 +1,11 @@ +package sonia.scm.boot; + +import javax.servlet.http.HttpServletRequest; + +public interface ViewController { + + String getTemplate(); + + View createView(HttpServletRequest request); + +} diff --git a/scm-webapp/src/test/java/sonia/scm/boot/SingleViewServletTest.java b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewServletTest.java new file mode 100644 index 0000000000..9dfebdc571 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewServletTest.java @@ -0,0 +1,90 @@ +package sonia.scm.boot; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.template.Template; +import sonia.scm.template.TemplateEngine; +import sonia.scm.template.TemplateEngineFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SingleViewServletTest { + + @Mock + private TemplateEngineFactory templateEngineFactory; + + @Mock + private TemplateEngine templateEngine; + + @Mock + private Template template; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private PrintWriter writer; + + @Mock + private ViewController controller; + + @Test + void shouldRenderTheTemplateOnGet() throws IOException { + prepareTemplate("/template"); + doReturn(new View(200, "hello")).when(controller).createView(request); + + new SingleViewServlet(templateEngineFactory, controller).doGet(request, response); + + verifyResponse(200, "hello"); + } + + private void verifyResponse(int sc, Object model) throws IOException { + verify(response).setStatus(sc); + verify(response).setContentType("text/html"); + verify(response).setCharacterEncoding("UTF-8"); + + verify(template).execute(writer, model); + } + + @Test + void shouldRenderTheTemplateOnPost() throws IOException { + prepareTemplate("/template"); + + doReturn(new View(201, "hello")).when(controller).createView(request); + + new SingleViewServlet(templateEngineFactory, controller).doPost(request, response); + + verifyResponse(201, "hello"); + } + + @Test + void shouldThrowIllegalStateExceptionOnIOException() throws IOException { + doReturn("/template").when(controller).getTemplate(); + doReturn(templateEngine).when(templateEngineFactory).getEngineByExtension("/template"); + doThrow(IOException.class).when(templateEngine).getTemplate("/template"); + + assertThrows(IllegalStateException.class, () -> new SingleViewServlet(templateEngineFactory, controller)); + } + + private void prepareTemplate(String templatePath) throws IOException { + doReturn(templateEngine).when(templateEngineFactory).getEngineByExtension(templatePath); + doReturn(template).when(templateEngine).getTemplate(templatePath); + doReturn(templatePath).when(controller).getTemplate(); + + doReturn(writer).when(response).getWriter(); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java new file mode 100644 index 0000000000..64c5ab98f8 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java @@ -0,0 +1,99 @@ +package sonia.scm.boot; + +import com.google.inject.Injector; +import com.google.inject.servlet.GuiceFilter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SingleViewTest { + + @Mock + private ServletContext servletContext; + + @Mock + private HttpServletRequest request; + + @Captor + private ArgumentCaptor captor; + + private GuiceFilter guiceFilter; + + @BeforeEach + void setUpGuiceFilter() throws ServletException { + guiceFilter = new GuiceFilter(); + FilterConfig config = mock(FilterConfig.class); + doReturn(servletContext).when(config).getServletContext(); + guiceFilter.init(config); + } + + @AfterEach + void tearDownGuiceFilter() { + guiceFilter.destroy(); + } + + @Test + void shouldCreateViewControllerForError() { + ServletContextListener listener = SingleView.error(new IOException("awesome io")); + when(request.getContextPath()).thenReturn("/scm"); + + ViewController instance = findViewController(listener); + assertErrorViewController(instance, "awesome io"); + } + + @Test + void shouldBindServlets() { + ServletContextListener listener = SingleView.error(new IOException("awesome io")); + Injector injector = findInjector(listener); + + assertThat(injector.getInstance(StaticResourceServlet.class)).isNotNull(); + assertThat(injector.getInstance(SingleViewServlet.class)).isNotNull(); + } + + @SuppressWarnings("unchecked") + private void assertErrorViewController(ViewController instance, String contains) { + assertThat(instance.getTemplate()).isEqualTo("/templates/error.mustache"); + + View view = instance.createView(request); + assertThat(view.getStatusCode()).isEqualTo(500); + assertThat(view.getModel()).isInstanceOfSatisfying(Map.class, map -> { + assertThat(map).containsEntry("contextPath", "/scm"); + String error = (String) map.get("error"); + assertThat(error).contains(contains); + } + ); + } + + private ViewController findViewController(ServletContextListener listener) { + Injector injector = findInjector(listener); + return injector.getInstance(ViewController.class); + } + + private Injector findInjector(ServletContextListener listener) { + listener.contextInitialized(new ServletContextEvent(servletContext)); + + verify(servletContext).setAttribute(anyString(), captor.capture()); + + return captor.getValue(); + } + + +} diff --git a/scm-webapp/src/test/java/sonia/scm/boot/StaticResourceServletTest.java b/scm-webapp/src/test/java/sonia/scm/boot/StaticResourceServletTest.java new file mode 100644 index 0000000000..a350f4d9f3 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/boot/StaticResourceServletTest.java @@ -0,0 +1,61 @@ +package sonia.scm.boot; + +import com.google.common.io.Resources; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.servlet.ServletContext; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URL; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class StaticResourceServletTest { + + @Mock + private HttpServletRequest request; + + @Mock + private ServletOutputStream stream; + + @Mock + private HttpServletResponse response; + + @Mock + private ServletContext context; + + @Test + void shouldServeResource() throws IOException { + doReturn("/scm").when(request).getContextPath(); + doReturn("/scm/resource.txt").when(request).getRequestURI(); + doReturn(context).when(request).getServletContext(); + URL resource = Resources.getResource("sonia/scm/boot/resource.txt"); + doReturn(resource).when(context).getResource("/resource.txt"); + doReturn(stream).when(response).getOutputStream(); + + StaticResourceServlet servlet = new StaticResourceServlet(); + servlet.doGet(request, response); + + verify(response).setStatus(HttpServletResponse.SC_OK); + } + + @Test + void shouldReturnNotFound() throws IOException { + doReturn("/scm").when(request).getContextPath(); + doReturn("/scm/resource.txt").when(request).getRequestURI(); + doReturn(context).when(request).getServletContext(); + + StaticResourceServlet servlet = new StaticResourceServlet(); + servlet.doGet(request, response); + + verify(response).setStatus(HttpServletResponse.SC_NOT_FOUND); + } + +} diff --git a/scm-webapp/src/test/resources/sonia/scm/boot/resource.txt b/scm-webapp/src/test/resources/sonia/scm/boot/resource.txt new file mode 100644 index 0000000000..c4a1132fa5 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/boot/resource.txt @@ -0,0 +1 @@ +Resource for testing From 0dda448ac839e4ac83b884f9ce56c052a0c3aa4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 13 Jun 2019 06:24:35 +0200 Subject: [PATCH 50/57] Heed peer review --- .../MigrateVerbsToPermissionRoles.java | 21 +++++++++++++++---- .../update/repository/RepositoryUpdates.java | 10 --------- .../MigrateVerbsToPermissionRolesTest.java | 2 +- 3 files changed, 18 insertions(+), 15 deletions(-) delete mode 100644 scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryUpdates.java diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java index 56227b7b13..b8b00e1554 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java @@ -30,17 +30,19 @@ import java.util.Optional; import java.util.Set; @Extension -public class MigrateVerbsToPermissionRoles extends RepositoryUpdates.RepositoryUpdateType implements UpdateStep { +public class MigrateVerbsToPermissionRoles implements UpdateStep { public static final Logger LOG = LoggerFactory.getLogger(MigrateVerbsToPermissionRoles.class); private final SingleRepositoryUpdateProcessor updateProcessor; private final SystemRepositoryPermissionProvider systemRepositoryPermissionProvider; + private final JAXBContext jaxbContext; @Inject public MigrateVerbsToPermissionRoles(SingleRepositoryUpdateProcessor updateProcessor, SystemRepositoryPermissionProvider systemRepositoryPermissionProvider) { this.updateProcessor = updateProcessor; this.systemRepositoryPermissionProvider = systemRepositoryPermissionProvider; + jaxbContext = createJAXBContext(); } @Override @@ -57,7 +59,6 @@ public class MigrateVerbsToPermissionRoles extends RepositoryUpdates.RepositoryU 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()); @@ -68,7 +69,6 @@ public class MigrateVerbsToPermissionRoles extends RepositoryUpdates.RepositoryU 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); @@ -115,9 +115,22 @@ public class MigrateVerbsToPermissionRoles extends RepositoryUpdates.RepositoryU return verbs.size() == r.getVerbs().size() && r.getVerbs().containsAll(verbs); } + private JAXBContext createJAXBContext() { + try { + return JAXBContext.newInstance(Repository.class); + } catch (JAXBException e) { + throw new UpdateException("could not create XML marshaller", e); + } + } + @Override public Version getTargetVersion() { - return Version.parse("2.0.0"); + return Version.parse("2.0.2"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.repository.xml"; } @XmlAccessorType(XmlAccessType.FIELD) diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryUpdates.java b/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryUpdates.java deleted file mode 100644 index 864f9aeb65..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryUpdates.java +++ /dev/null @@ -1,10 +0,0 @@ -package sonia.scm.update.repository; - -public class RepositoryUpdates { - - static class RepositoryUpdateType { - public String getAffectedDataType() { - return "repository"; - } - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrateVerbsToPermissionRolesTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/MigrateVerbsToPermissionRolesTest.java index 536af6e865..8d4ceb1a14 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrateVerbsToPermissionRolesTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/MigrateVerbsToPermissionRolesTest.java @@ -52,7 +52,7 @@ class MigrateVerbsToPermissionRolesTest { } @Test - void x(@TempDirectory.TempDir Path tempDir) throws IOException { + void shouldUpdateToRolesIfPossible(@TempDirectory.TempDir Path tempDir) throws IOException { migration.doUpdate(); List newMetadata = Files.readAllLines(tempDir.resolve("metadata.xml")); From a14a2060b6a961afc0035a629f0a71c7c5785b40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 13 Jun 2019 06:41:46 +0200 Subject: [PATCH 51/57] Fix context --- .../repository/MigrateVerbsToPermissionRoles.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java index b8b00e1554..0b96a58385 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrateVerbsToPermissionRoles.java @@ -36,13 +36,15 @@ public class MigrateVerbsToPermissionRoles implements UpdateStep { private final SingleRepositoryUpdateProcessor updateProcessor; private final SystemRepositoryPermissionProvider systemRepositoryPermissionProvider; - private final JAXBContext jaxbContext; + private final JAXBContext jaxbContextNewRepository; + private final JAXBContext jaxbContextOldRepository; @Inject public MigrateVerbsToPermissionRoles(SingleRepositoryUpdateProcessor updateProcessor, SystemRepositoryPermissionProvider systemRepositoryPermissionProvider) { this.updateProcessor = updateProcessor; this.systemRepositoryPermissionProvider = systemRepositoryPermissionProvider; - jaxbContext = createJAXBContext(); + jaxbContextNewRepository = createJAXBContext(Repository.class); + jaxbContextOldRepository = createJAXBContext(OldRepository.class); } @Override @@ -59,7 +61,7 @@ public class MigrateVerbsToPermissionRoles implements UpdateStep { private void writeNewRepository(Path path, Repository newRepository) { try { - Marshaller marshaller = jaxbContext.createMarshaller(); + Marshaller marshaller = jaxbContextNewRepository.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); marshaller.marshal(newRepository, path.resolve("metadata.xml").toFile()); } catch (JAXBException e) { @@ -69,7 +71,7 @@ public class MigrateVerbsToPermissionRoles implements UpdateStep { private OldRepository readOldRepository(Path path) { try { - return (OldRepository) jaxbContext.createUnmarshaller().unmarshal(path.resolve("metadata.xml").toFile()); + return (OldRepository) jaxbContextOldRepository.createUnmarshaller().unmarshal(path.resolve("metadata.xml").toFile()); } catch (JAXBException e) { throw new UpdateException("could not read old repository structure", e); } @@ -115,9 +117,9 @@ public class MigrateVerbsToPermissionRoles implements UpdateStep { return verbs.size() == r.getVerbs().size() && r.getVerbs().containsAll(verbs); } - private JAXBContext createJAXBContext() { + private JAXBContext createJAXBContext(Class clazz) { try { - return JAXBContext.newInstance(Repository.class); + return JAXBContext.newInstance(clazz); } catch (JAXBException e) { throw new UpdateException("could not create XML marshaller", e); } From e266b1edb539103782b62077576f322e3c1197a7 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 13 Jun 2019 06:07:26 +0000 Subject: [PATCH 52/57] Close branch feature/migrate_custom_roles From cd99402f78bd98fce3fe24fad1800c972440654b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 13 Jun 2019 09:38:44 +0200 Subject: [PATCH 53/57] Revert create forms --- scm-ui/src/groups/components/GroupForm.js | 12 ++---------- scm-ui/src/users/components/UserForm.js | 19 +------------------ 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/scm-ui/src/groups/components/GroupForm.js b/scm-ui/src/groups/components/GroupForm.js index 637be3faff..04a5e6f35f 100644 --- a/scm-ui/src/groups/components/GroupForm.js +++ b/scm-ui/src/groups/components/GroupForm.js @@ -9,7 +9,6 @@ import { InputField, SubmitButton, Textarea, - Icon, Checkbox } from "@scm-manager/ui-components"; import type { Group, SelectValue } from "@scm-manager/ui-types"; @@ -114,16 +113,9 @@ class GroupForm extends React.Component { if (this.isExistingGroup()) { return null; } - - const label = group && group.external ? ( - <>{t("group.external")} - ) : ( - <>{t("group.internal")} - ); - return ( { ); }; - isExistingGroup = () => !! this.props.group; + isExistingGroup = () => !!this.props.group; render() { const { loading, t } = this.props; diff --git a/scm-ui/src/users/components/UserForm.js b/scm-ui/src/users/components/UserForm.js index 02dc310c03..c473e92322 100644 --- a/scm-ui/src/users/components/UserForm.js +++ b/scm-ui/src/users/components/UserForm.js @@ -5,7 +5,6 @@ import type { User } from "@scm-manager/ui-types"; import { Subtitle, Checkbox, - Icon, InputField, PasswordConfirmation, SubmitButton, @@ -17,8 +16,6 @@ type Props = { submitForm: User => void, user?: User, loading?: boolean, - - // context props t: string => string }; @@ -83,7 +80,6 @@ class UserForm extends React.Component { return ( this.props.user.displayName === user.displayName && this.props.user.mail === user.mail && - this.props.user.admin === user.admin && this.props.user.active === user.active ); } else { @@ -139,19 +135,6 @@ class UserForm extends React.Component { // edit existing user subtitle = ; } - - const label = - user && user.active ? ( - <> - {t("user.active")} - - ) : ( - <> - {t("user.inactive")}{" "} - - - ); - return ( <> {subtitle} @@ -183,7 +166,7 @@ class UserForm extends React.Component {
    {passwordChangeField} Date: Thu, 13 Jun 2019 09:58:30 +0200 Subject: [PATCH 54/57] display error on startup, if previous version is older than 1.60 --- .../scm/boot/BootstrapContextListener.java | 4 + .../main/java/sonia/scm/boot/SingleView.java | 10 ++ .../main/java/sonia/scm/boot/Versions.java | 77 +++++++++++++ .../main/resources/templates/error.mustache | 2 +- .../main/resources/templates/to-old.mustache | 14 +++ scm-webapp/src/main/webapp/error.mustache | 102 ------------------ .../java/sonia/scm/boot/SingleViewTest.java | 12 +++ .../java/sonia/scm/boot/VersionsTest.java | 86 +++++++++++++++ 8 files changed, 204 insertions(+), 103 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/boot/Versions.java create mode 100644 scm-webapp/src/main/resources/templates/to-old.mustache delete mode 100644 scm-webapp/src/main/webapp/error.mustache create mode 100644 scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index 844a04c79f..686b638cb3 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -63,6 +63,7 @@ import sonia.scm.util.IOUtil; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; +import javax.servlet.http.HttpServletResponse; import javax.xml.bind.DataBindingException; import javax.xml.bind.JAXB; import javax.xml.bind.annotation.XmlAccessType; @@ -141,8 +142,11 @@ public class BootstrapContextListener implements ServletContextListener { Throwable startupError = SCMContext.getContext().getStartupError(); if (startupError != null) { contextListener = SingleView.error(startupError); + } else if (Versions.isToOld()) { + contextListener = SingleView.view("/templates/to-old.mustache", HttpServletResponse.SC_CONFLICT); } else { createMigrationOrNormalContextListener(); + Versions.writeNew(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java b/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java index f1c57ce9c6..72e2c3522f 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/SingleView.java @@ -37,6 +37,16 @@ final class SingleView { return new SingleViewContextListener(controller); } + static ServletContextListener view(String template, int sc) { + ViewController controller = new SimpleViewController(template, request -> { + Object model = ImmutableMap.of( + "contextPath", request.getContextPath() + ); + return new View(sc, model); + }); + return new SingleViewContextListener(controller); + } + private static class SingleViewContextListener extends GuiceServletContextListener { private final ViewController controller; diff --git a/scm-webapp/src/main/java/sonia/scm/boot/Versions.java b/scm-webapp/src/main/java/sonia/scm/boot/Versions.java new file mode 100644 index 0000000000..e3c2848ace --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/Versions.java @@ -0,0 +1,77 @@ +package sonia.scm.boot; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContext; +import sonia.scm.SCMContextProvider; +import sonia.scm.util.IOUtil; +import sonia.scm.version.Version; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +class Versions { + + private static final Logger LOG = LoggerFactory.getLogger(Versions.class); + + private static final Version MIN_VERSION = Version.parse("1.60"); + + private final SCMContextProvider contextProvider; + + @VisibleForTesting + Versions(SCMContextProvider contextProvider) { + this.contextProvider = contextProvider; + } + + @VisibleForTesting + boolean isPreviousVersionToOld() { + return readVersion().map(v -> v.isOlder(MIN_VERSION)).orElse(false); + } + + @VisibleForTesting + void writeNewVersion() { + Path config = contextProvider.resolve(Paths.get("config")); + IOUtil.mkdirs(config.toFile()); + + String version = contextProvider.getVersion(); + LOG.debug("write new version {} to file", version); + Path versionFile = config.resolve("version.txt"); + try { + Files.write(versionFile, version.getBytes()); + } catch (IOException e) { + throw new IllegalStateException("failed to write version file", e); + } + } + + private Optional readVersion() { + Path versionFile = contextProvider.resolve(Paths.get("config", "version.txt")); + if (versionFile.toFile().exists()) { + return Optional.of(readVersionFromFile(versionFile)); + } + return Optional.empty(); + } + + private Version readVersionFromFile(Path versionFile) { + try { + String versionString = new String(Files.readAllBytes(versionFile), StandardCharsets.UTF_8).trim(); + LOG.debug("read previous version {} from file", versionString); + return Version.parse(versionString); + } catch (IOException e) { + throw new IllegalStateException("failed to read version file", e); + } + } + + static boolean isToOld() { + return new Versions(SCMContext.getContext()).isPreviousVersionToOld(); + } + + static void writeNew() { + new Versions(SCMContext.getContext()).writeNewVersion(); + } + +} diff --git a/scm-webapp/src/main/resources/templates/error.mustache b/scm-webapp/src/main/resources/templates/error.mustache index ade00d002b..4b9b0a9086 100644 --- a/scm-webapp/src/main/resources/templates/error.mustache +++ b/scm-webapp/src/main/resources/templates/error.mustache @@ -3,7 +3,7 @@ {{$title}}SCM-Manager Error{{/title}} {{$content}} -

    There is an error occurred during SCM-Manager startup.

    +

    An error occurred during SCM-Manager startup.

    diff --git a/scm-webapp/src/main/resources/templates/to-old.mustache b/scm-webapp/src/main/resources/templates/to-old.mustache
    new file mode 100644
    index 0000000000..2ff578b263
    --- /dev/null
    +++ b/scm-webapp/src/main/resources/templates/to-old.mustache
    @@ -0,0 +1,14 @@
    +{{< layout}}
    +
    +  {{$title}}SCM-Manager Error{{/title}}
    +
    +  {{$content}}
    +    

    An error occurred during SCM-Manager startup.

    + +

    + We cannot migrate your SCM-Manager 1 installation, + because the version is too old.
    + Please migrate to version 1.60 or newer, before migration to 2.x. +

    + {{/content}} +{{/ layout}} diff --git a/scm-webapp/src/main/webapp/error.mustache b/scm-webapp/src/main/webapp/error.mustache deleted file mode 100644 index 7a726a2664..0000000000 --- a/scm-webapp/src/main/webapp/error.mustache +++ /dev/null @@ -1,102 +0,0 @@ - - - - - SCM-Manager Error - - - - -

    SCM-Manager Error

    - -

    - There is an error occurred during SCM-Manager startup. -

    - -
    -      {{error}}
    -    
    - - - diff --git a/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java index 64c5ab98f8..f520aea504 100644 --- a/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java +++ b/scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java @@ -50,6 +50,18 @@ class SingleViewTest { guiceFilter.destroy(); } + @Test + void shouldCreateViewControllerForView() { + ServletContextListener listener = SingleView.view("/my-template", 409); + when(request.getContextPath()).thenReturn("/scm"); + + ViewController instance = findViewController(listener); + assertThat(instance.getTemplate()).isEqualTo("/my-template"); + + View view = instance.createView(request); + assertThat(view.getStatusCode()).isEqualTo(409); + } + @Test void shouldCreateViewControllerForError() { ServletContextListener listener = SingleView.error(new IOException("awesome io")); diff --git a/scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java b/scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java new file mode 100644 index 0000000000..0506bd5594 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java @@ -0,0 +1,86 @@ +package sonia.scm.boot; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.SCMContextProvider; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; + +@ExtendWith({MockitoExtension.class, TempDirectory.class}) +class VersionsTest { + + @Mock + private SCMContextProvider contextProvider; + + @InjectMocks + private Versions versions; + + @Test + void shouldReturnTrueForVersionsPreviousTo160(@TempDirectory.TempDir Path directory) throws IOException { + setVersion(directory, "1.59"); + assertThat(versions.isPreviousVersionToOld()).isTrue(); + + setVersion(directory, "1.12"); + assertThat(versions.isPreviousVersionToOld()).isTrue(); + } + + @Test + void shouldReturnFalseForVersion160(@TempDirectory.TempDir Path directory) throws IOException { + setVersion(directory, "1.60"); + assertThat(versions.isPreviousVersionToOld()).isFalse(); + } + + @Test + void shouldNotFailIfVersionContainsLineBreak(@TempDirectory.TempDir Path directory) throws IOException { + setVersion(directory, "1.59\n"); + assertThat(versions.isPreviousVersionToOld()).isTrue(); + } + + @Test + void shouldReturnFalseForVersionsNewerAs160(@TempDirectory.TempDir Path directory) throws IOException { + setVersion(directory, "1.61"); + assertThat(versions.isPreviousVersionToOld()).isFalse(); + + setVersion(directory, "1.82"); + assertThat(versions.isPreviousVersionToOld()).isFalse(); + } + + @Test + void shouldReturnFalseForNonExistingVersionFile(@TempDirectory.TempDir Path directory) { + setVersionFile(directory.resolve("version.txt")); + assertThat(versions.isPreviousVersionToOld()).isFalse(); + } + + @Test + void shouldWriteNewVersion(@TempDirectory.TempDir Path directory) { + Path config = directory.resolve("config"); + doReturn(config).when(contextProvider).resolve(Paths.get("config")); + doReturn("2.0.0").when(contextProvider).getVersion(); + + versions.writeNewVersion(); + + Path versionFile = config.resolve("version.txt"); + assertThat(versionFile).exists().hasContent("2.0.0"); + } + + private void setVersion(Path directory, String version) throws IOException { + Path file = directory.resolve("version.txt"); + Files.write(file, version.getBytes(StandardCharsets.UTF_8)); + setVersionFile(file); + } + + private void setVersionFile(Path file) { + doReturn(file).when(contextProvider).resolve(Paths.get("config", "version.txt")); + } +} From d7318bdacd778c3f10be6bf87613b123c2aa9d6e Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 13 Jun 2019 08:03:10 +0000 Subject: [PATCH 55/57] Close branch feature/prettier_tables From 67a78fd3b2f30883d04d3ac399c3d395589aba36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 13 Jun 2019 10:38:15 +0200 Subject: [PATCH 56/57] Fix typo --- .../sonia/scm/boot/BootstrapContextListener.java | 4 ++-- .../src/main/java/sonia/scm/boot/Versions.java | 6 +++--- .../{to-old.mustache => too-old.mustache} | 0 .../src/test/java/sonia/scm/boot/VersionsTest.java | 14 +++++++------- 4 files changed, 12 insertions(+), 12 deletions(-) rename scm-webapp/src/main/resources/templates/{to-old.mustache => too-old.mustache} (100%) diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index 686b638cb3..363a9e5e19 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -142,8 +142,8 @@ public class BootstrapContextListener implements ServletContextListener { Throwable startupError = SCMContext.getContext().getStartupError(); if (startupError != null) { contextListener = SingleView.error(startupError); - } else if (Versions.isToOld()) { - contextListener = SingleView.view("/templates/to-old.mustache", HttpServletResponse.SC_CONFLICT); + } else if (Versions.isTooOld()) { + contextListener = SingleView.view("/templates/too-old.mustache", HttpServletResponse.SC_CONFLICT); } else { createMigrationOrNormalContextListener(); Versions.writeNew(); diff --git a/scm-webapp/src/main/java/sonia/scm/boot/Versions.java b/scm-webapp/src/main/java/sonia/scm/boot/Versions.java index e3c2848ace..6da19fedca 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/Versions.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/Versions.java @@ -29,7 +29,7 @@ class Versions { } @VisibleForTesting - boolean isPreviousVersionToOld() { + boolean isPreviousVersionTooOld() { return readVersion().map(v -> v.isOlder(MIN_VERSION)).orElse(false); } @@ -66,8 +66,8 @@ class Versions { } } - static boolean isToOld() { - return new Versions(SCMContext.getContext()).isPreviousVersionToOld(); + static boolean isTooOld() { + return new Versions(SCMContext.getContext()).isPreviousVersionTooOld(); } static void writeNew() { diff --git a/scm-webapp/src/main/resources/templates/to-old.mustache b/scm-webapp/src/main/resources/templates/too-old.mustache similarity index 100% rename from scm-webapp/src/main/resources/templates/to-old.mustache rename to scm-webapp/src/main/resources/templates/too-old.mustache diff --git a/scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java b/scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java index 0506bd5594..e5aa8fe3d1 100644 --- a/scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java +++ b/scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java @@ -29,37 +29,37 @@ class VersionsTest { @Test void shouldReturnTrueForVersionsPreviousTo160(@TempDirectory.TempDir Path directory) throws IOException { setVersion(directory, "1.59"); - assertThat(versions.isPreviousVersionToOld()).isTrue(); + assertThat(versions.isPreviousVersionTooOld()).isTrue(); setVersion(directory, "1.12"); - assertThat(versions.isPreviousVersionToOld()).isTrue(); + assertThat(versions.isPreviousVersionTooOld()).isTrue(); } @Test void shouldReturnFalseForVersion160(@TempDirectory.TempDir Path directory) throws IOException { setVersion(directory, "1.60"); - assertThat(versions.isPreviousVersionToOld()).isFalse(); + assertThat(versions.isPreviousVersionTooOld()).isFalse(); } @Test void shouldNotFailIfVersionContainsLineBreak(@TempDirectory.TempDir Path directory) throws IOException { setVersion(directory, "1.59\n"); - assertThat(versions.isPreviousVersionToOld()).isTrue(); + assertThat(versions.isPreviousVersionTooOld()).isTrue(); } @Test void shouldReturnFalseForVersionsNewerAs160(@TempDirectory.TempDir Path directory) throws IOException { setVersion(directory, "1.61"); - assertThat(versions.isPreviousVersionToOld()).isFalse(); + assertThat(versions.isPreviousVersionTooOld()).isFalse(); setVersion(directory, "1.82"); - assertThat(versions.isPreviousVersionToOld()).isFalse(); + assertThat(versions.isPreviousVersionTooOld()).isFalse(); } @Test void shouldReturnFalseForNonExistingVersionFile(@TempDirectory.TempDir Path directory) { setVersionFile(directory.resolve("version.txt")); - assertThat(versions.isPreviousVersionToOld()).isFalse(); + assertThat(versions.isPreviousVersionTooOld()).isFalse(); } @Test From ce9f3f29a1f5aba9df9562a9da4e2ed94da101cb Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 13 Jun 2019 09:04:02 +0000 Subject: [PATCH 57/57] Close branch feature/fail_migration_if_to_old