diff --git a/scm-core/src/main/java/sonia/scm/migration/UpdateException.java b/scm-core/src/main/java/sonia/scm/migration/UpdateException.java new file mode 100644 index 0000000000..4023620b96 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/migration/UpdateException.java @@ -0,0 +1,11 @@ +package sonia.scm.migration; + +public class UpdateException extends RuntimeException { + public UpdateException(String message) { + super(message); + } + + public UpdateException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java b/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java new file mode 100644 index 0000000000..ed5f60a630 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java @@ -0,0 +1,82 @@ +package sonia.scm.migration; + +import sonia.scm.plugin.ExtensionPoint; +import sonia.scm.version.Version; + +/** + * This is the main interface for data migration/update. Using this interface, SCM-Manager provides the possibility to + * change data structures between versions for a given type of data. + *

The data type can be an arbitrary string, but it is considered a best practice to use a qualified name, for + * example + *

+ *

+ *

The version is unrelated to other versions and therefore can be chosen freely, so that a data type can be updated + * without in various ways independent of other data types or the official version of the plugin or the core. + * A coordination between different data types and their versions is only necessary, when update steps of different data + * types rely on each other. If a update step of data type A has to run before another step for data type + * B, the version number of the second step has to be greater in regards to {@link Version#compareTo(Version)}. + *

+ *

The algorithm looks something like this:
+ * Whenever the SCM-Manager starts, + *

+ *

+ */ +@ExtensionPoint +public interface UpdateStep { + /** + * Implement this to update the data to the new version. If any {@link Exception} is thrown, SCM-Manager will not + * start up. + */ + void doUpdate() throws Exception; + + /** + * Declares the new version of the data type given by {@link #getAffectedDataType()}. A update step will only be + * executed, when this version is bigger than the last recorded version for its data type according to + * {@link Version#compareTo(Version)} + */ + Version getTargetVersion(); + + /** + * Declares the data type this update step will take care of. This should be a qualified name, like + * com.example.myPlugin.configuration. + */ + String getAffectedDataType(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java index 663d053ca5..875fb55617 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java @@ -46,6 +46,7 @@ import sonia.scm.store.ConfigurationStoreFactory; import java.io.File; import java.io.IOException; import java.net.URL; +import java.nio.file.Path; //~--- JDK imports ------------------------------------------------------------ @@ -172,6 +173,6 @@ public abstract class AbstractSimpleRepositoryHandler extends RepositoryLocationResolver { + + private final Class type; + + protected BasicRepositoryLocationResolver(Class type) { + this.type = type; + } + + @Override + public boolean supportsLocationType(Class type) { + return type.isAssignableFrom(this.type); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/PathBasedRepositoryDAO.java b/scm-core/src/main/java/sonia/scm/repository/PathBasedRepositoryDAO.java deleted file mode 100644 index 35a47af233..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/PathBasedRepositoryDAO.java +++ /dev/null @@ -1,18 +0,0 @@ -package sonia.scm.repository; - -import java.nio.file.Path; - -/** - * A DAO used for Repositories accessible by a path - * - * @author Mohamed Karray - * @since 2.0.0 - */ -public interface PathBasedRepositoryDAO extends RepositoryDAO { - - /** - * Get the current path of the repository for the given id. - * This works for existing repositories only, not for repositories that should be created. - */ - Path getPath(String repositoryId) ; -} 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 737374025d..bc8df673d0 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java @@ -1,51 +1,20 @@ package sonia.scm.repository; -import sonia.scm.SCMContextProvider; +public abstract class RepositoryLocationResolver { -import javax.inject.Inject; -import java.nio.file.Path; + public abstract boolean supportsLocationType(Class type); -/** - * A Location Resolver for File based Repository Storage. - *

- * WARNING: The Locations provided with this class may not be used from the plugins to store any plugin specific files. - *

- * Please use the {@link sonia.scm.store.DataStoreFactory } and the {@link sonia.scm.store.DataStore} classes to store data
- * Please use the {@link sonia.scm.store.BlobStoreFactory } and the {@link sonia.scm.store.BlobStore} classes to store binary files
- * Please use the {@link sonia.scm.store.ConfigurationStoreFactory} and the {@link sonia.scm.store.ConfigurationStore} classes to store configurations - * - * @author Mohamed Karray - * @since 2.0.0 - */ -public class RepositoryLocationResolver { + protected abstract RepositoryLocationResolverInstance create(Class type); - private final SCMContextProvider contextProvider; - private final RepositoryDAO repositoryDAO; - private final InitialRepositoryLocationResolver initialRepositoryLocationResolver; - - @Inject - public RepositoryLocationResolver(SCMContextProvider contextProvider, RepositoryDAO repositoryDAO, InitialRepositoryLocationResolver initialRepositoryLocationResolver) { - this.contextProvider = contextProvider; - this.repositoryDAO = repositoryDAO; - this.initialRepositoryLocationResolver = initialRepositoryLocationResolver; + public final RepositoryLocationResolverInstance forClass(Class type) { + if (!supportsLocationType(type)) { + throw new IllegalStateException("no support for location of class " + type); + } + return create(type); } - /** - * Returns the path to the repository. - * - * @param repositoryId repository id - * - * @return path of repository - */ - public Path getPath(String repositoryId) { - Path path; - - if (repositoryDAO instanceof PathBasedRepositoryDAO) { - path = ((PathBasedRepositoryDAO) repositoryDAO).getPath(repositoryId); - } else { - path = initialRepositoryLocationResolver.getPath(repositoryId); - } - - return contextProvider.resolve(path); + @FunctionalInterface + public interface RepositoryLocationResolverInstance { + T getLocation(String repositoryId); } } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java index 42ee8ffa65..1f8f638cb8 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryRole.java @@ -121,8 +121,11 @@ public class RepositoryRole implements ModelObject, PermissionObject { * @return the hash code value for the {@link RepositoryRole} */ @Override - public int hashCode() { - return Objects.hashCode(name, verbs); + public int hashCode() + { + // Normally we do not have a log of repository permissions having the same size of verbs, but different content. + // Therefore we do not use the verbs themselves for the hash code but only the number of verbs. + return Objects.hashCode(name, verbs == null? -1: verbs.size()); } @Override diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryLocationResolverTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryLocationResolverTest.java deleted file mode 100644 index 05f9af3773..0000000000 --- a/scm-core/src/test/java/sonia/scm/repository/RepositoryLocationResolverTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package sonia.scm.repository; - -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 org.mockito.stubbing.Answer; -import sonia.scm.SCMContextProvider; - -import java.nio.file.Path; -import java.nio.file.Paths; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -@ExtendWith({MockitoExtension.class}) -class RepositoryLocationResolverTest { - - @Mock - private SCMContextProvider contextProvider; - - @Mock - private PathBasedRepositoryDAO pathBasedRepositoryDAO; - - @Mock - private RepositoryDAO repositoryDAO; - - @Mock - private InitialRepositoryLocationResolver initialRepositoryLocationResolver; - - - @BeforeEach - void beforeEach() { - when(contextProvider.resolve(any(Path.class))).then((Answer) invocationOnMock -> invocationOnMock.getArgument(0)); - } - - private RepositoryLocationResolver createResolver(RepositoryDAO pathBasedRepositoryDAO) { - return new RepositoryLocationResolver(contextProvider, pathBasedRepositoryDAO, initialRepositoryLocationResolver); - } - - @Test - void shouldReturnPathFromDao() { - Path repositoryPath = Paths.get("repos", "42"); - when(pathBasedRepositoryDAO.getPath("42")).thenReturn(repositoryPath); - - RepositoryLocationResolver resolver = createResolver(pathBasedRepositoryDAO); - Path path = resolver.getPath("42"); - - assertThat(path).isSameAs(repositoryPath); - } - - @Test - void shouldReturnInitialPathIfDaoIsNotPathBased() { - Path repositoryPath = Paths.get("r", "42"); - when(initialRepositoryLocationResolver.getPath("42")).thenReturn(repositoryPath); - - RepositoryLocationResolver resolver = createResolver(repositoryDAO); - Path path = resolver.getPath("42"); - - assertThat(path).isSameAs(repositoryPath); - } - -} 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 new file mode 100644 index 0000000000..cde4b3c3e2 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -0,0 +1,141 @@ +package sonia.scm.repository.xml; + +import sonia.scm.SCMContextProvider; +import sonia.scm.repository.BasicRepositoryLocationResolver; +import sonia.scm.repository.InitialRepositoryLocationResolver; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.store.StoreConstants; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Clock; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; + +/** + * A Location Resolver for File based Repository Storage. + *

+ * WARNING: The Locations provided with this class may not be used from the plugins to store any plugin specific files. + *

+ * Please use the {@link sonia.scm.store.DataStoreFactory } and the {@link sonia.scm.store.DataStore} classes to store data
+ * Please use the {@link sonia.scm.store.BlobStoreFactory } and the {@link sonia.scm.store.BlobStore} classes to store binary files
+ * Please use the {@link sonia.scm.store.ConfigurationStoreFactory} and the {@link sonia.scm.store.ConfigurationStore} classes to store configurations + * + * @since 2.0.0 + */ +public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver { + + private static final String STORE_NAME = "repositories"; + + private final SCMContextProvider contextProvider; + private final InitialRepositoryLocationResolver initialRepositoryLocationResolver; + + private final PathDatabase pathDatabase; + private final Map pathById; + + private final Clock clock; + + private Long creationTime; + private Long lastModified; + + @Inject + public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver) { + this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC()); + } + + public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) { + super(Path.class); + this.contextProvider = contextProvider; + this.initialRepositoryLocationResolver = initialRepositoryLocationResolver; + this.pathById = new ConcurrentHashMap<>(); + + this.clock = clock; + + this.creationTime = clock.millis(); + pathDatabase = new PathDatabase(resolveStorePath()); + + read(); + } + + @Override + protected RepositoryLocationResolverInstance create(Class type) { + return repositoryId -> { + if (pathById.containsKey(repositoryId)) { + return (T) contextProvider.resolve(pathById.get(repositoryId)); + } else { + return (T) create(repositoryId); + } + }; + } + + Path create(String repositoryId) { + Path path = initialRepositoryLocationResolver.getPath(repositoryId); + pathById.put(repositoryId, path); + writePathDatabase(); + Path resolvedPath = contextProvider.resolve(path); + try { + Files.createDirectories(resolvedPath); + } catch (IOException e) { + throw new InternalRepositoryException(entity("Repository", repositoryId), "could not create directory for new repository", e); + } + return resolvedPath; + } + + Path remove(String repositoryId) { + Path removedPath = pathById.remove(repositoryId); + writePathDatabase(); + return contextProvider.resolve(removedPath); + } + + void forAllPaths(BiConsumer consumer) { + pathById.forEach((id, path) -> consumer.accept(id, contextProvider.resolve(path))); + } + + void updateModificationDate() { + this.writePathDatabase(); + } + + private void writePathDatabase() { + lastModified = clock.millis(); + pathDatabase.write(creationTime, lastModified, pathById); + } + + private void read() { + Path storePath = resolveStorePath(); + + // Files.exists is slow on java 8 + if (storePath.toFile().exists()) { + pathDatabase.read(this::onLoadDates, this::onLoadRepository); + } + } + + private void onLoadDates(Long creationTime, Long lastModified) { + this.creationTime = creationTime; + this.lastModified = lastModified; + } + + public Long getCreationTime() { + return creationTime; + } + + public Long getLastModified() { + return lastModified; + } + + + private void onLoadRepository(String id, Path repositoryPath) { + pathById.put(id, repositoryPath); + } + + private Path resolveStorePath() { + return contextProvider.getBaseDirectory() + .toPath() + .resolve(StoreConstants.CONFIG_DIRECTORY_NAME) + .resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION)); + } +} 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 4987b269da..9b3105b5ed 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 @@ -33,22 +33,18 @@ package sonia.scm.repository.xml; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.inject.Singleton; -import sonia.scm.SCMContextProvider; import sonia.scm.io.FileSystem; -import sonia.scm.repository.InitialRepositoryLocationResolver; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.PathBasedRepositoryDAO; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryDAO; import sonia.scm.store.StoreConstants; import javax.inject.Inject; import java.io.IOException; import java.nio.file.Path; -import java.time.Clock; import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -57,82 +53,38 @@ import java.util.concurrent.ConcurrentHashMap; * @author Sebastian Sdorra */ @Singleton -public class XmlRepositoryDAO implements PathBasedRepositoryDAO { +public class XmlRepositoryDAO implements RepositoryDAO { - private static final String STORE_NAME = "repositories"; - private final PathDatabase pathDatabase; private final MetadataStore metadataStore = new MetadataStore(); - private final SCMContextProvider context; - private final InitialRepositoryLocationResolver locationResolver; + private final PathBasedRepositoryLocationResolver repositoryLocationResolver; private final FileSystem fileSystem; - private final Map pathById; private final Map byId; private final Map byNamespaceAndName; - private final Clock clock; - - private Long creationTime; - private Long lastModified; - @Inject - public XmlRepositoryDAO(SCMContextProvider context, InitialRepositoryLocationResolver locationResolver, FileSystem fileSystem) { - this(context, locationResolver, fileSystem, Clock.systemUTC()); - } - - XmlRepositoryDAO(SCMContextProvider context, InitialRepositoryLocationResolver locationResolver, FileSystem fileSystem, Clock clock) { - this.context = context; - this.locationResolver = locationResolver; + public XmlRepositoryDAO(PathBasedRepositoryLocationResolver repositoryLocationResolver, FileSystem fileSystem) { + this.repositoryLocationResolver = repositoryLocationResolver; this.fileSystem = fileSystem; - this.clock = clock; - this.creationTime = clock.millis(); - - this.pathById = new ConcurrentHashMap<>(); this.byId = new ConcurrentHashMap<>(); this.byNamespaceAndName = new ConcurrentHashMap<>(); - pathDatabase = new PathDatabase(resolveStorePath()); - read(); + init(); } - private void read() { - Path storePath = resolveStorePath(); - - // Files.exists is slow on java 8 - if (storePath.toFile().exists()) { - pathDatabase.read(this::onLoadDates, this::onLoadRepository); - } + private void init() { + repositoryLocationResolver.forAllPaths((repositoryId, repositoryPath) -> { + Path metadataPath = resolveDataPath(repositoryPath); + Repository repository = metadataStore.read(metadataPath); + byNamespaceAndName.put(repository.getNamespaceAndName(), repository); + byId.put(repositoryId, repository); + }); } - private void onLoadDates(Long creationTime, Long lastModified) { - this.creationTime = creationTime; - this.lastModified = lastModified; - } - - private void onLoadRepository(String id, Path repositoryPath) { - Path metadataPath = resolveMetadataPath(context.resolve(repositoryPath)); - - Repository repository = metadataStore.read(metadataPath); - - byId.put(id, repository); - byNamespaceAndName.put(repository.getNamespaceAndName(), repository); - pathById.put(id, repositoryPath); - } - - @VisibleForTesting - Path resolveStorePath() { - return context.getBaseDirectory() - .toPath() - .resolve(StoreConstants.CONFIG_DIRECTORY_NAME) - .resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION)); - } - - - @VisibleForTesting - Path resolveMetadataPath(Path repositoryPath) { + private Path resolveDataPath(Path repositoryPath) { return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION)); } @@ -141,47 +93,27 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO { return "xml"; } - @Override - public Long getCreationTime() { - return creationTime; - } - - @Override - public Long getLastModified() { - return lastModified; - } - @Override public void add(Repository repository) { Repository clone = repository.clone(); - Path repositoryPath = locationResolver.getPath(repository.getId()); - Path resolvedPath = context.resolve(repositoryPath); + synchronized (this) { + Path repositoryPath = repositoryLocationResolver.create(repository.getId()); - try { - fileSystem.create(resolvedPath.toFile()); - - Path metadataPath = resolveMetadataPath(resolvedPath); - metadataStore.write(metadataPath, repository); - - synchronized (this) { - pathById.put(repository.getId(), repositoryPath); - - byId.put(repository.getId(), clone); - byNamespaceAndName.put(repository.getNamespaceAndName(), clone); - - writePathDatabase(); + try { + Path metadataPath = resolveDataPath(repositoryPath); + metadataStore.write(metadataPath, repository); + } catch (Exception e) { + repositoryLocationResolver.remove(repository.getId()); + throw new InternalRepositoryException(repository, "failed to create filesystem", e); } - } catch (IOException e) { - throw new InternalRepositoryException(repository, "failed to create filesystem", e); + byId.put(repository.getId(), clone); + byNamespaceAndName.put(repository.getNamespaceAndName(), clone); } + } - private void writePathDatabase() { - lastModified = clock.millis(); - pathDatabase.write(creationTime, lastModified, pathById); - } @Override public boolean contains(Repository repository) { @@ -224,12 +156,13 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO { byNamespaceAndName.remove(prev.getNamespaceAndName()); } byNamespaceAndName.put(clone.getNamespaceAndName(), clone); - - writePathDatabase(); } - Path repositoryPath = context.resolve(getPath(repository.getId())); - Path metadataPath = resolveMetadataPath(repositoryPath); + Path repositoryPath = repositoryLocationResolver + .create(Path.class) + .getLocation(repository.getId()); + Path metadataPath = resolveDataPath(repositoryPath); + repositoryLocationResolver.updateModificationDate(); metadataStore.write(metadataPath, clone); } @@ -241,14 +174,9 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO { if (prev != null) { byNamespaceAndName.remove(prev.getNamespaceAndName()); } - - path = pathById.remove(repository.getId()); - - writePathDatabase(); + path = repositoryLocationResolver.remove(repository.getId()); } - path = context.resolve(path); - try { fileSystem.destroy(path.toFile()); } catch (IOException e) { @@ -257,7 +185,12 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO { } @Override - public Path getPath(String repositoryId) { - return pathById.get(repositoryId); + public Long getCreationTime() { + return repositoryLocationResolver.getCreationTime(); + } + + @Override + public Long getLastModified() { + return repositoryLocationResolver.getLastModified(); } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java index d37a150723..d31179c1c2 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java @@ -40,6 +40,7 @@ import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.util.IOUtil; import java.io.File; +import java.nio.file.Path; //~--- JDK imports ------------------------------------------------------------ @@ -58,7 +59,7 @@ public abstract class FileBasedStoreFactory { private RepositoryLocationResolver repositoryLocationResolver; private Store store; - protected FileBasedStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, Store store) { + protected FileBasedStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, Store store) { this.contextProvider = contextProvider; this.repositoryLocationResolver = repositoryLocationResolver; this.store = store; @@ -92,7 +93,7 @@ public abstract class FileBasedStoreFactory { * @return the store directory of a specific repository */ private File getStoreDirectory(Store store, Repository repository) { - return new File(repositoryLocationResolver.getPath(repository.getId()).toFile(), store.getRepositoryStoreDirectory()); + return new File(repositoryLocationResolver.forClass(Path.class).getLocation(repository.getId()).toFile(), store.getRepositoryStoreDirectory()); } /** diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java index 7e2e5a9e29..4a7c9fc713 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java @@ -65,7 +65,7 @@ public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobS * @param keyGenerator key generator */ @Inject - public FileBlobStoreFactory(SCMContextProvider contextProvider ,RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) { + public FileBlobStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) { super(contextProvider, repositoryLocationResolver, Store.BLOB); this.keyGenerator = keyGenerator; } 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 new file mode 100644 index 0000000000..f8b517e18b --- /dev/null +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java @@ -0,0 +1,172 @@ +package sonia.scm.repository.xml; + +import com.google.common.base.Charsets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import sonia.scm.SCMContextProvider; +import sonia.scm.repository.InitialRepositoryLocationResolver; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Clock; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class PathBasedRepositoryLocationResolverTest { + + private static final long CREATION_TIME = 42; + + @Mock + private SCMContextProvider contextProvider; + + @Mock + private InitialRepositoryLocationResolver initialRepositoryLocationResolver; + + @Mock + private Clock clock; + + private Path basePath; + + private PathBasedRepositoryLocationResolver resolver; + + @BeforeEach + void beforeEach(@TempDirectory.TempDir Path temp) { + this.basePath = temp; + when(contextProvider.getBaseDirectory()).thenReturn(temp.toFile()); + when(contextProvider.resolve(any(Path.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(initialRepositoryLocationResolver.getPath(anyString())).thenAnswer(invocation -> temp.resolve(invocation.getArgument(0).toString())); + when(clock.millis()).thenReturn(CREATION_TIME); + resolver = createResolver(); + } + + @Test + void shouldCreateInitialDirectory() { + Path path = resolver.forClass(Path.class).getLocation("newId"); + + assertThat(path).isEqualTo(basePath.resolve("newId")); + assertThat(path).isDirectory(); + } + + @Test + void shouldPersistInitialDirectory() { + resolver.forClass(Path.class).getLocation("newId"); + + String content = getXmlFileContent(); + + assertThat(content).contains("newId"); + assertThat(content).contains(basePath.resolve("newId").toString()); + } + + @Test + void shouldPersistWithCreationDate() { + long now = CREATION_TIME + 100; + when(clock.millis()).thenReturn(now); + + resolver.forClass(Path.class).getLocation("newId"); + + assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME); + + String content = getXmlFileContent(); + assertThat(content).contains("creation-time=\"" + CREATION_TIME + "\""); + } + + @Test + void shouldUpdateWithModifiedDate() { + long now = CREATION_TIME + 100; + when(clock.millis()).thenReturn(now); + + resolver.forClass(Path.class).getLocation("newId"); + + assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME); + assertThat(resolver.getLastModified()).isEqualTo(now); + + String content = getXmlFileContent(); + assertThat(content).contains("creation-time=\"" + CREATION_TIME + "\""); + assertThat(content).contains("last-modified=\"" + now + "\""); + } + + @Nested + class WithExistingData { + + private PathBasedRepositoryLocationResolver resolverWithExistingData; + + @BeforeEach + void createExistingDatabase() { + resolver.forClass(Path.class).getLocation("existingId_1"); + resolver.forClass(Path.class).getLocation("existingId_2"); + resolverWithExistingData = createResolver(); + } + + @Test + void shouldInitWithExistingData() { + Map foundRepositories = new HashMap<>(); + resolverWithExistingData.forAllPaths( + foundRepositories::put + ); + assertThat(foundRepositories) + .containsKeys("existingId_1", "existingId_2"); + } + + @Test + void shouldRemoveFromFile() { + resolverWithExistingData.remove("existingId_1"); + + assertThat(getXmlFileContent()).doesNotContain("existingId_1"); + } + + @Test + void shouldNotUpdateModificationDateForExistingDirectoryMapping() { + long now = CREATION_TIME + 100; + Path path = resolverWithExistingData.create(Path.class).getLocation("existingId_1"); + + assertThat(path).isEqualTo(basePath.resolve("existingId_1")); + + String content = getXmlFileContent(); + assertThat(content).doesNotContain("last-modified=\"" + now + "\""); + } + + @Test + void shouldNotCreateDirectoryForExistingMapping() throws IOException { + Files.delete(basePath.resolve("existingId_1")); + + Path path = resolverWithExistingData.create(Path.class).getLocation("existingId_1"); + + assertThat(path).doesNotExist(); + } + } + + private String getXmlFileContent() { + Path storePath = basePath.resolve("config").resolve("repositories.xml"); + + assertThat(storePath).isRegularFile(); + return content(storePath); + } + + private PathBasedRepositoryLocationResolver createResolver() { + return new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver, clock); + } + + private String content(Path storePath) { + try { + return new String(Files.readAllBytes(storePath), Charsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} 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 aebdf010e2..5b9a00aec8 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 @@ -2,376 +2,309 @@ package sonia.scm.repository.xml; import com.google.common.base.Charsets; +import com.google.common.io.Resources; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; 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 sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermission; -import sonia.scm.repository.RepositoryTestData; 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.time.Clock; import java.util.Collection; -import java.util.Collections; -import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith({MockitoExtension.class, TempDirectory.class}) @MockitoSettings(strictness = Strictness.LENIENT) class XmlRepositoryDAOTest { - @Mock - private SCMContextProvider context; + private final Repository REPOSITORY = createRepository("42"); @Mock - private InitialRepositoryLocationResolver locationResolver; + private PathBasedRepositoryLocationResolver locationResolver; + + @Captor + private ArgumentCaptor> forAllCaptor; private FileSystem fileSystem = new DefaultFileSystem(); private XmlRepositoryDAO dao; - private Path baseDirectory; - - private AtomicLong atomicClock; - @BeforeEach - void createDAO(@TempDirectory.TempDir Path baseDirectory) { - this.baseDirectory = baseDirectory; - this.atomicClock = new AtomicLong(); - - when(locationResolver.getPath("42")).thenReturn(Paths.get("repos", "42")); - when(locationResolver.getPath("42+1")).thenReturn(Paths.get("repos", "puzzle")); - - when(context.getBaseDirectory()).thenReturn(baseDirectory.toFile()); - when(context.resolve(any(Path.class))).then(ic -> { - Path path = ic.getArgument(0); - return baseDirectory.resolve(path); - }); - - dao = createDAO(); + void createDAO(@TempDirectory.TempDir Path basePath) { + when(locationResolver.create(Path.class)).thenReturn(locationResolver::create); + when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation)); + when(locationResolver.remove(anyString())).thenAnswer(invocation -> basePath.resolve(invocation.getArgument(0).toString())); } - private XmlRepositoryDAO createDAO() { - Clock clock = mock(Clock.class); - when(clock.millis()).then(ic -> atomicClock.incrementAndGet()); + private Path createMockedRepoPath(@TempDirectory.TempDir Path basePath, InvocationOnMock invocation) { + Path resolvedPath = basePath.resolve(invocation.getArgument(0).toString()); + try { + Files.createDirectories(resolvedPath); + } catch (IOException e) { + fail(e); + } + return resolvedPath; + } - return new XmlRepositoryDAO(context, locationResolver, fileSystem, clock); + @Nested + class WithEmptyDatabase { + + @BeforeEach + void createDAO() { + dao = new XmlRepositoryDAO(locationResolver, fileSystem); + } + + @Test + void shouldReturnXmlType() { + assertThat(dao.getType()).isEqualTo("xml"); + } + + @Test + void shouldReturnCreationTimeOfLocationResolver() { + long now = 42L; + when(locationResolver.getCreationTime()).thenReturn(now); + assertThat(dao.getCreationTime()).isEqualTo(now); + } + + @Test + void shouldReturnLasModifiedOfLocationResolver() { + long now = 42L; + when(locationResolver.getLastModified()).thenReturn(now); + assertThat(dao.getLastModified()).isEqualTo(now); + } + + @Test + void shouldReturnTrueForEachContainsMethod() { + dao.add(REPOSITORY); + + assertThat(dao.contains(REPOSITORY)).isTrue(); + assertThat(dao.contains(REPOSITORY.getId())).isTrue(); + assertThat(dao.contains(REPOSITORY.getNamespaceAndName())).isTrue(); + } + + @Test + void shouldPersistRepository() { + dao.add(REPOSITORY); + + String content = getXmlFileContent(REPOSITORY.getId()); + + assertThat(content).contains("42"); + } + + @Test + void shouldDeleteDataFile() { + dao.add(REPOSITORY); + dao.delete(REPOSITORY); + + assertThat(metadataFile(REPOSITORY.getId())).doesNotExist(); + } + + @Test + void shouldModifyRepository() { + dao.add(REPOSITORY); + Repository changedRepository = REPOSITORY.clone(); + changedRepository.setContact("change"); + + dao.modify(changedRepository); + + String content = getXmlFileContent(REPOSITORY.getId()); + + assertThat(content).contains("change"); + } + + @Test + void shouldReturnFalseForEachContainsMethod() { + assertThat(dao.contains(REPOSITORY)).isFalse(); + assertThat(dao.contains(REPOSITORY.getId())).isFalse(); + assertThat(dao.contains(REPOSITORY.getNamespaceAndName())).isFalse(); + } + + @Test + void shouldReturnNullForEachGetMethod() { + assertThat(dao.get("42")).isNull(); + assertThat(dao.get(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isNull(); + } + + @Test + void shouldReturnRepository() { + dao.add(REPOSITORY); + + assertThat(dao.get("42")).isEqualTo(REPOSITORY); + assertThat(dao.get(new NamespaceAndName("space", "42"))).isEqualTo(REPOSITORY); + } + + @Test + void shouldNotReturnTheSameInstance() { + dao.add(REPOSITORY); + + Repository repository = dao.get("42"); + assertThat(repository).isNotSameAs(REPOSITORY); + } + + @Test + void shouldReturnAllRepositories() { + dao.add(REPOSITORY); + + Repository secondRepository = createRepository("23"); + dao.add(secondRepository); + + Collection repositories = dao.getAll(); + assertThat(repositories) + .containsExactlyInAnyOrder(REPOSITORY, secondRepository); + } + + @Test + void shouldModifyRepositoryTwice() { + REPOSITORY.setDescription("HeartOfGold"); + dao.add(REPOSITORY); + assertThat(dao.get("42").getDescription()).isEqualTo("HeartOfGold"); + + Repository heartOfGold = createRepository("42"); + heartOfGold.setDescription("Heart of Gold"); + dao.modify(heartOfGold); + + assertThat(dao.get("42").getDescription()).isEqualTo("Heart of Gold"); + } + + @Test + void shouldRemoveRepository() { + dao.add(REPOSITORY); + assertThat(dao.contains("42")).isTrue(); + + dao.delete(REPOSITORY); + assertThat(dao.contains("42")).isFalse(); + assertThat(dao.contains(REPOSITORY.getNamespaceAndName())).isFalse(); + + Path storePath = metadataFile(REPOSITORY.getId()); + + assertThat(storePath).doesNotExist(); + } + + @Test + void shouldRenameTheRepository() { + dao.add(REPOSITORY); + + REPOSITORY.setNamespace("hg2tg"); + REPOSITORY.setName("hog"); + + dao.modify(REPOSITORY); + + Repository repository = dao.get("42"); + assertThat(repository.getNamespace()).isEqualTo("hg2tg"); + assertThat(repository.getName()).isEqualTo("hog"); + + assertThat(dao.contains(new NamespaceAndName("hg2tg", "hog"))).isTrue(); + assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse(); + + String content = getXmlFileContent(REPOSITORY.getId()); + assertThat(content).contains("hog"); + } + + @Test + void shouldDeleteRepositoryEvenWithChangedNamespace() { + dao.add(REPOSITORY); + + REPOSITORY.setNamespace("hg2tg"); + REPOSITORY.setName("hog"); + + dao.delete(REPOSITORY); + + assertThat(dao.contains(new NamespaceAndName("space", "42"))).isFalse(); + } + + @Test + void shouldRemoveRepositoryDirectoryAfterDeletion() { + dao.add(REPOSITORY); + + Path path = locationResolver.create(REPOSITORY.getId()); + assertThat(path).isDirectory(); + + dao.delete(REPOSITORY); + assertThat(path).doesNotExist(); + } + + @Test + void shouldPersistPermissions() { + REPOSITORY.setPermissions(asList(new RepositoryPermission("trillian", asList("read", "write"), false), new RepositoryPermission("vogons", singletonList("delete"), true))); + dao.add(REPOSITORY); + + String content = getXmlFileContent(REPOSITORY.getId()); + assertThat(content).containsSubsequence("trillian", "read", "write"); + assertThat(content).containsSubsequence("vogons", "delete"); + } + + @Test + void shouldUpdateRepositoryPathDatabse() { + dao.add(REPOSITORY); + + verify(locationResolver, never()).updateModificationDate(); + + dao.modify(REPOSITORY); + + verify(locationResolver).updateModificationDate(); + } } @Test - void shouldReturnXmlType() { - assertThat(dao.getType()).isEqualTo("xml"); + void shouldReadExistingRepositoriesFromPathDatabase(@TempDirectory.TempDir Path basePath) throws IOException { + doNothing().when(locationResolver).forAllPaths(forAllCaptor.capture()); + XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem); + + 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")); + + forAllCaptor.getValue().accept("existing", repositoryPath); + + assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue(); } - @Test - void shouldReturnCreationTimeAfterCreation() { - long now = atomicClock.get(); - assertThat(dao.getCreationTime()).isEqualTo(now); - } + private String getXmlFileContent(String id) { + Path storePath = metadataFile(id); - @Test - void shouldNotReturnLastModifiedAfterCreation() { - assertThat(dao.getLastModified()).isNull(); - } - - @Test - void shouldReturnTrueForEachContainsMethod() { - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - assertThat(dao.contains(heartOfGold)).isTrue(); - assertThat(dao.contains(heartOfGold.getId())).isTrue(); - assertThat(dao.contains(heartOfGold.getNamespaceAndName())).isTrue(); - } - - private Repository createHeartOfGold() { - Repository heartOfGold = RepositoryTestData.createHeartOfGold(); - heartOfGold.setId("42"); - return heartOfGold; - } - - @Test - void shouldReturnFalseForEachContainsMethod() { - Repository heartOfGold = createHeartOfGold(); - - assertThat(dao.contains(heartOfGold)).isFalse(); - assertThat(dao.contains(heartOfGold.getId())).isFalse(); - assertThat(dao.contains(heartOfGold.getNamespaceAndName())).isFalse(); - } - - @Test - void shouldReturnNullForEachGetMethod() { - assertThat(dao.get("42")).isNull(); - assertThat(dao.get(new NamespaceAndName("hitchhiker","HeartOfGold"))).isNull(); - } - - @Test - void shouldReturnRepository() { - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - assertThat(dao.get("42")).isEqualTo(heartOfGold); - assertThat(dao.get(new NamespaceAndName("hitchhiker","HeartOfGold"))).isEqualTo(heartOfGold); - } - - @Test - void shouldNotReturnTheSameInstance() { - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - Repository repository = dao.get("42"); - assertThat(repository).isNotSameAs(heartOfGold); - } - - @Test - void shouldReturnAllRepositories() { - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - Repository puzzle = createPuzzle(); - dao.add(puzzle); - - Collection repositories = dao.getAll(); - assertThat(repositories).containsExactlyInAnyOrder(heartOfGold, puzzle); - } - - private Repository createPuzzle() { - Repository puzzle = RepositoryTestData.create42Puzzle(); - puzzle.setId("42+1"); - return puzzle; - } - - @Test - void shouldModifyRepository() { - Repository heartOfGold = createHeartOfGold(); - heartOfGold.setDescription("HeartOfGold"); - dao.add(heartOfGold); - assertThat(dao.get("42").getDescription()).isEqualTo("HeartOfGold"); - - heartOfGold = createHeartOfGold(); - heartOfGold.setDescription("Heart of Gold"); - dao.modify(heartOfGold); - - assertThat(dao.get("42").getDescription()).isEqualTo("Heart of Gold"); - } - - @Test - void shouldRemoveRepository() { - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - assertThat(dao.contains("42")).isTrue(); - - dao.delete(heartOfGold); - assertThat(dao.contains("42")).isFalse(); - assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse(); - } - - @Test - void shouldUpdateLastModifiedAfterEachWriteOperation() { - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - Long firstLastModified = dao.getLastModified(); - assertThat(firstLastModified).isNotNull(); - - Repository puzzle = createPuzzle(); - dao.add(puzzle); - - Long lastModifiedAdded = dao.getLastModified(); - assertThat(lastModifiedAdded).isGreaterThan(firstLastModified); - - heartOfGold.setDescription("Heart of Gold"); - dao.modify(heartOfGold); - - Long lastModifiedModified = dao.getLastModified(); - assertThat(lastModifiedModified).isGreaterThan(lastModifiedAdded); - - dao.delete(puzzle); - - Long lastModifiedRemoved = dao.getLastModified(); - assertThat(lastModifiedRemoved).isGreaterThan(lastModifiedModified); - } - - @Test - void shouldRenameTheRepository() { - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - heartOfGold.setNamespace("hg2tg"); - heartOfGold.setName("hog"); - - dao.modify(heartOfGold); - - Repository repository = dao.get("42"); - assertThat(repository.getNamespace()).isEqualTo("hg2tg"); - assertThat(repository.getName()).isEqualTo("hog"); - - assertThat(dao.contains(new NamespaceAndName("hg2tg", "hog"))).isTrue(); - assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse(); - } - - @Test - void shouldDeleteRepositoryEvenWithChangedNamespace() { - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - heartOfGold.setNamespace("hg2tg"); - heartOfGold.setName("hog"); - - dao.delete(heartOfGold); - - assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse(); - } - - @Test - void shouldReturnThePathForTheRepository() { - Path repositoryPath = Paths.get("r", "42"); - when(locationResolver.getPath("42")).thenReturn(repositoryPath); - - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - Path path = dao.getPath("42"); - assertThat(path).isEqualTo(repositoryPath); - } - - @Test - void shouldCreateTheDirectoryForTheRepository() { - Path repositoryPath = Paths.get("r", "42"); - when(locationResolver.getPath("42")).thenReturn(repositoryPath); - - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - Path path = getAbsolutePathFromDao("42"); - assertThat(path).isDirectory(); - } - - @Test - void shouldRemoveRepositoryDirectoryAfterDeletion() { - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - Path path = getAbsolutePathFromDao(heartOfGold.getId()); - assertThat(path).isDirectory(); - - dao.delete(heartOfGold); - assertThat(path).doesNotExist(); - } - - private Path getAbsolutePathFromDao(String id) { - return context.resolve(dao.getPath(id)); - } - - @Test - void shouldCreateRepositoryPathDatabase() throws IOException { - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - Path storePath = dao.resolveStorePath(); assertThat(storePath).isRegularFile(); - - String content = content(storePath); - - assertThat(content).contains(heartOfGold.getId()); - assertThat(content).contains(dao.getPath(heartOfGold.getId()).toString()); + return content(storePath); } - private String content(Path storePath) throws IOException { - return new String(Files.readAllBytes(storePath), Charsets.UTF_8); + private Path metadataFile(String id) { + return locationResolver.create(id).resolve("metadata.xml"); } - @Test - void shouldStoreRepositoryMetadataAfterAdd() throws IOException { - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId()); - Path metadataPath = dao.resolveMetadataPath(repositoryDirectory); - - assertThat(metadataPath).isRegularFile(); - - String content = content(metadataPath); - assertThat(content).contains(heartOfGold.getName()); - assertThat(content).contains(heartOfGold.getNamespace()); - assertThat(content).contains(heartOfGold.getDescription()); + private String content(Path storePath) { + try { + return new String(Files.readAllBytes(storePath), Charsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } } - @Test - void shouldUpdateRepositoryMetadataAfterModify() throws IOException { - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - heartOfGold.setDescription("Awesome Spaceship"); - dao.modify(heartOfGold); - - Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId()); - Path metadataPath = dao.resolveMetadataPath(repositoryDirectory); - - String content = content(metadataPath); - assertThat(content).contains("Awesome Spaceship"); - } - - @Test - void shouldPersistPermissions() throws IOException { - Repository heartOfGold = createHeartOfGold(); - heartOfGold.setPermissions(asList(new RepositoryPermission("trillian", asList("read", "write"), false), new RepositoryPermission("vogons", Collections.singletonList("delete"), true))); - dao.add(heartOfGold); - - Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId()); - Path metadataPath = dao.resolveMetadataPath(repositoryDirectory); - - String content = content(metadataPath); - System.out.println(content); - assertThat(content).containsSubsequence("trillian", "read", "write"); - assertThat(content).containsSubsequence("vogons", "delete"); - } - - @Test - void shouldReadPathDatabaseAndMetadataOfRepositories() { - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - // reload data - dao = createDAO(); - - heartOfGold = dao.get("42"); - assertThat(heartOfGold.getName()).isEqualTo("HeartOfGold"); - - Path path = getAbsolutePathFromDao(heartOfGold.getId()); - assertThat(path).isDirectory(); - } - - @Test - void shouldReadCreationTimeAndLastModifedDateFromDatabase() { - Repository heartOfGold = createHeartOfGold(); - dao.add(heartOfGold); - - Long creationTime = dao.getCreationTime(); - Long lastModified = dao.getLastModified(); - - // reload data - dao = createDAO(); - - assertThat(dao.getCreationTime()).isEqualTo(creationTime); - assertThat(dao.getLastModified()).isEqualTo(lastModified); + private static Repository createRepository(String id) { + return new Repository(id, "xml", "space", id); } } diff --git a/scm-dao-xml/src/test/resources/sonia/scm/store/repositoryDaoMetadata.xml b/scm-dao-xml/src/test/resources/sonia/scm/store/repositoryDaoMetadata.xml new file mode 100644 index 0000000000..87aa3775ea --- /dev/null +++ b/scm-dao-xml/src/test/resources/sonia/scm/store/repositoryDaoMetadata.xml @@ -0,0 +1,10 @@ + + + + existing + space + existing + false + false + xml + diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolver.java index 8acfc68dce..737776f3ac 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolver.java @@ -26,7 +26,7 @@ public class GitRepositoryContextResolver implements RepositoryContextResolver { public RepositoryContext resolve(String[] args) { NamespaceAndName namespaceAndName = extractNamespaceAndName(args); Repository repository = repositoryManager.get(namespaceAndName); - Path path = locationResolver.getPath(repository.getId()).resolve("data"); + Path path = locationResolver.forClass(Path.class).getLocation(repository.getId()).resolve("data"); return new RepositoryContext(repository, path); } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolverTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolverTest.java index 6ac4cdb54b..7e7566f6f9 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolverTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolverTest.java @@ -2,6 +2,7 @@ package sonia.scm.protocolcommand.git; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -16,6 +17,7 @@ import java.io.IOException; import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -25,7 +27,7 @@ class GitRepositoryContextResolverTest { @Mock RepositoryManager repositoryManager; - @Mock + @Mock(answer = Answers.RETURNS_DEEP_STUBS) RepositoryLocationResolver locationResolver; @InjectMocks @@ -35,7 +37,7 @@ class GitRepositoryContextResolverTest { void shouldResolveCorrectRepository() throws IOException { when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(REPOSITORY); Path repositoryPath = File.createTempFile("test", "scm").toPath(); - when(locationResolver.getPath("id")).thenReturn(repositoryPath); + when(locationResolver.forClass(any()).getLocation("id")).thenReturn(repositoryPath); RepositoryContext context = resolver.resolve(new String[] {"git", "repo/space/X/something/else"}); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java index cca89d8eb5..ee5117b276 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java @@ -37,6 +37,7 @@ package sonia.scm.repository; import org.junit.Assume; import sonia.scm.SCMContext; +import sonia.scm.TempDirRepositoryLocationResolver; import sonia.scm.store.InMemoryConfigurationStoreFactory; import javax.servlet.http.HttpServletRequest; @@ -101,13 +102,12 @@ public final class HgTestUtil context.setBaseDirectory(directory); - PathBasedRepositoryDAO repoDao = mock(PathBasedRepositoryDAO.class); + RepositoryDAO repoDao = mock(RepositoryDAO.class); - RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(context, repoDao, new InitialRepositoryLocationResolver()); + RepositoryLocationResolver repositoryLocationResolver = new TempDirRepositoryLocationResolver(directory); HgRepositoryHandler handler = new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null, null); Path repoDir = directory.toPath(); - when(repoDao.getPath(any())).thenReturn(repoDir); handler.init(context); return handler; diff --git a/scm-test/src/main/java/sonia/scm/AbstractTestBase.java b/scm-test/src/main/java/sonia/scm/AbstractTestBase.java index 040b347e4a..48831fb670 100644 --- a/scm-test/src/main/java/sonia/scm/AbstractTestBase.java +++ b/scm-test/src/main/java/sonia/scm/AbstractTestBase.java @@ -91,7 +91,7 @@ public class AbstractTestBase contextProvider = MockUtil.getSCMContextProvider(tempDirectory); fileSystem = new DefaultFileSystem(); InitialRepositoryLocationResolver initialRepoLocationResolver = new InitialRepositoryLocationResolver(); - repositoryLocationResolver = new RepositoryLocationResolver(contextProvider, repositoryDAO, initialRepoLocationResolver); + repositoryLocationResolver = new TempDirRepositoryLocationResolver(tempDirectory); postSetUp(); } @@ -254,4 +254,5 @@ public class AbstractTestBase subjectThreadState = createThreadState(subject); subjectThreadState.bind(); } + } diff --git a/scm-test/src/main/java/sonia/scm/ManagerTestBase.java b/scm-test/src/main/java/sonia/scm/ManagerTestBase.java index 823e88c9fc..3990b05df9 100644 --- a/scm-test/src/main/java/sonia/scm/ManagerTestBase.java +++ b/scm-test/src/main/java/sonia/scm/ManagerTestBase.java @@ -74,7 +74,7 @@ public abstract class ManagerTestBase contextProvider = MockUtil.getSCMContextProvider(temp); InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(); RepositoryDAO repoDao = mock(RepositoryDAO.class); - locationResolver = new RepositoryLocationResolver(contextProvider, repoDao ,initialRepositoryLocationResolver); + locationResolver = new TempDirRepositoryLocationResolver(temp); manager = createManager(); manager.init(contextProvider); } diff --git a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java new file mode 100644 index 0000000000..1dfc22e15a --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java @@ -0,0 +1,21 @@ +package sonia.scm; + +import sonia.scm.repository.BasicRepositoryLocationResolver; +import sonia.scm.repository.RepositoryLocationResolver; + +import java.io.File; +import java.nio.file.Path; + +public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationResolver { + private final File tempDirectory; + + public TempDirRepositoryLocationResolver(File tempDirectory) { + super(Path.class); + this.tempDirectory = tempDirectory; + } + + @Override + protected RepositoryLocationResolverInstance create(Class type) { + return repositoryId -> (T) tempDirectory.toPath(); + } +} 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 f48744d460..000835156a 100644 --- a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java +++ b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java @@ -44,6 +44,7 @@ import java.io.IOException; import java.nio.file.Path; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -56,7 +57,7 @@ import static org.mockito.Mockito.when; public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase { - protected PathBasedRepositoryDAO repoDao = mock(PathBasedRepositoryDAO.class); + protected RepositoryDAO repoDao = mock(RepositoryDAO.class); protected Path repoPath; protected Repository repository; @@ -78,7 +79,11 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase { locationResolver = mock(RepositoryLocationResolver.class); - when(locationResolver.getPath(anyString())).then(ic -> { + 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 -> { String id = ic.getArgument(0); return baseDirectory.toPath().resolve(id); }); @@ -107,7 +112,7 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase { repository = RepositoryTestData.createHeartOfGold(); File repoDirectory = new File(baseDirectory, repository.getId()); repoPath = repoDirectory.toPath(); - when(repoDao.getPath(repository.getId())).thenReturn(repoPath); +// when(repoDao.getPath(repository.getId())).thenReturn(repoPath); return new File(repoDirectory, AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY); } diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index e6a784ddfe..f3c96af5fc 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -184,6 +184,12 @@ ${guice.version} + + com.google.inject.extensions + guice-assistedinject + ${guice.version} + + diff --git a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java index 8ae005e826..40fb345caa 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java @@ -37,6 +37,7 @@ import com.google.common.base.Throwables; import com.google.common.collect.Lists; import com.google.inject.Injector; import com.google.inject.Module; +import com.google.inject.assistedinject.Assisted; import org.apache.shiro.guice.web.ShiroWebModule; import org.jboss.resteasy.plugins.guice.GuiceResteasyBootstrapServletContextListener; import org.slf4j.Logger; @@ -55,6 +56,7 @@ import sonia.scm.upgrade.UpgradeManager; import sonia.scm.user.UserManager; import sonia.scm.util.IOUtil; +import javax.inject.Inject; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import java.util.Collections; @@ -77,9 +79,12 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList private final Set plugins; private Injector injector; - //~--- constructors --------------------------------------------------------- - - public ScmContextListener(ClassLoader parent, Set plugins) + public interface Factory { + ScmContextListener create(ClassLoader parent, Set plugins); + } + + @Inject + public ScmContextListener(@Assisted ClassLoader parent, @Assisted Set plugins) { this.parent = parent; this.plugins = plugins; @@ -127,9 +132,6 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList List moduleList = Lists.newArrayList(); moduleList.add(new ResteasyModule()); - moduleList.add(new ScmInitializerModule()); - moduleList.add(new ScmEventBusModule()); - moduleList.add(new EagerSingletonModule()); moduleList.add(ShiroWebModule.guiceFilterModule()); moduleList.add(new WebElementModule(pluginLoader)); moduleList.add(new ScmServletModule(context, pluginLoader, overrides)); diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java index 8cc34b6f23..b1eed1c437 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java @@ -212,12 +212,7 @@ public class ScmServletModule extends ServletModule { install(ThrowingProviderBinder.forModule(this)); - SCMContextProvider context = SCMContext.getContext(); - - bind(SCMContextProvider.class).toInstance(context); - ScmConfiguration config = getScmConfiguration(); - CipherUtil cu = CipherUtil.getInstance(); bind(NamespaceStrategy.class).toProvider(NamespaceStrategyProvider.class); @@ -234,21 +229,11 @@ public class ScmServletModule extends ServletModule bind(ScmEventBus.class).toInstance(ScmEventBus.getInstance()); // bind core - bind(ConfigurationStoreFactory.class, JAXBConfigurationStoreFactory.class); - bind(ConfigurationEntryStoreFactory.class, JAXBConfigurationEntryStoreFactory.class); - bind(DataStoreFactory.class, JAXBDataStoreFactory.class); - bind(BlobStoreFactory.class, FileBlobStoreFactory.class); bind(ScmConfiguration.class).toInstance(config); - bind(PluginLoader.class).toInstance(pluginLoader); bind(PluginManager.class, DefaultPluginManager.class); // bind scheduler bind(Scheduler.class).to(QuartzScheduler.class); - - // note CipherUtil uses an other generator - bind(KeyGenerator.class).to(DefaultKeyGenerator.class); - bind(CipherHandler.class).toInstance(cu.getCipherHandler()); - bind(FileSystem.class, DefaultFileSystem.class); // bind health check stuff bind(HealthCheckContextListener.class); @@ -327,7 +312,6 @@ public class ScmServletModule extends ServletModule bind(ObjectMapper.class).toProvider(ObjectMapperProvider.class); // bind events - // bind(LastModifiedUpdateListener.class); bind(AccessTokenCookieIssuer.class).to(DefaultAccessTokenCookieIssuer.class); bind(PushStateDispatcher.class).toProvider(PushStateDispatcherProvider.class); 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 be5a1e7ac2..3af8a76650 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -1,9 +1,9 @@ /** * 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 @@ -11,7 +11,7 @@ * 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 @@ -22,67 +22,65 @@ * 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.boot; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.io.Files; - +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.assistedinject.FactoryModuleBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import sonia.scm.EagerSingletonModule; import sonia.scm.SCMContext; import sonia.scm.ScmContextListener; +import sonia.scm.ScmEventBusModule; +import sonia.scm.ScmInitializerModule; import sonia.scm.Stage; import sonia.scm.event.ScmEventBus; +import sonia.scm.plugin.DefaultPluginLoader; import sonia.scm.plugin.Plugin; import sonia.scm.plugin.PluginException; import sonia.scm.plugin.PluginLoadException; +import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginWrapper; import sonia.scm.plugin.PluginsInternal; import sonia.scm.plugin.SmpArchive; +import sonia.scm.update.UpdateEngine; import sonia.scm.util.ClassLoaders; import sonia.scm.util.IOUtil; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Closeable; -import java.io.File; -import java.io.IOException; - -import java.net.MalformedURLException; -import java.net.URL; - -import java.util.Iterator; -import java.util.List; -import java.util.Set; - import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; - import javax.xml.bind.DataBindingException; import javax.xml.bind.JAXB; 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; +import java.net.URL; +import java.util.Iterator; +import java.util.List; +import java.util.Set; /** * * @author Sebastian Sdorra */ -public class BootstrapContextListener implements ServletContextListener -{ +public class BootstrapContextListener implements ServletContextListener { /** Field description */ private static final String DIRECTORY_PLUGINS = "plugins"; @@ -109,22 +107,16 @@ public class BootstrapContextListener implements ServletContextListener * @param sce */ @Override - public void contextDestroyed(ServletContextEvent sce) - { + public void contextDestroyed(ServletContextEvent sce) { contextListener.contextDestroyed(sce); - for (PluginWrapper plugin : contextListener.getPlugins()) - { + for (PluginWrapper plugin : contextListener.getPlugins()) { ClassLoader pcl = plugin.getClassLoader(); - if (pcl instanceof Closeable) - { - try - { + if (pcl instanceof Closeable) { + try { ((Closeable) pcl).close(); - } - catch (IOException ex) - { + } catch (IOException ex) { logger.warn("could not close plugin classloader", ex); } } @@ -141,43 +133,68 @@ public class BootstrapContextListener implements ServletContextListener * @param sce */ @Override - public void contextInitialized(ServletContextEvent sce) - { + public void contextInitialized(ServletContextEvent sce) { context = sce.getServletContext(); File pluginDirectory = getPluginDirectory(); - try - { + createContextListener(pluginDirectory); + + contextListener.contextInitialized(sce); + + // register for restart events + if (!registered && (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)) { + logger.info("register for restart events"); + ScmEventBus.getInstance().register(this); + registered = true; + } + } + + private void createContextListener(File pluginDirectory) { + try { if (!isCorePluginExtractionDisabled()) { extractCorePlugins(context, pluginDirectory); } else { logger.info("core plugin extraction is disabled"); } - ClassLoader cl = - ClassLoaders.getContextClassLoader(BootstrapContextListener.class); + ClassLoader cl = ClassLoaders.getContextClassLoader(BootstrapContextListener.class); - Set plugins = PluginsInternal.collectPlugins(cl, - pluginDirectory.toPath()); + Set plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath()); - contextListener = new ScmContextListener(cl, plugins); - } - catch (IOException ex) - { + PluginLoader pluginLoader = new DefaultPluginLoader(context, cl, plugins); + + Injector bootstrapInjector = createBootstrapInjector(pluginLoader); + + processUpdates(pluginLoader, bootstrapInjector); + + contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins); + } catch (IOException ex) { throw new PluginLoadException("could not load plugins", ex); } + } - contextListener.contextInitialized(sce); - - // register for restart events - if (!registered - && (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)) - { - logger.info("register for restart events"); - ScmEventBus.getInstance().register(this); - registered = true; - } + private Injector createBootstrapInjector(PluginLoader pluginLoader) { + Module scmContextListenerModule = new ScmContextListenerModule(); + BootstrapModule bootstrapModule = new BootstrapModule(pluginLoader); + ScmInitializerModule scmInitializerModule = new ScmInitializerModule(); + EagerSingletonModule eagerSingletonModule = new EagerSingletonModule(); + ScmEventBusModule scmEventBusModule = new ScmEventBusModule(); + + return Guice.createInjector( + bootstrapModule, + scmContextListenerModule, + scmEventBusModule, + scmInitializerModule, + eagerSingletonModule + ); + } + + private void processUpdates(PluginLoader pluginLoader, Injector bootstrapInjector) { + Injector updateInjector = bootstrapInjector.createChildInjector(new UpdateStepModule(pluginLoader)); + + UpdateEngine updateEngine = updateInjector.getInstance(UpdateEngine.class); + updateEngine.update(); } private boolean isCorePluginExtractionDisabled() { @@ -195,41 +212,32 @@ public class BootstrapContextListener implements ServletContextListener * @throws IOException */ private void extractCorePlugin(ServletContext context, File pluginDirectory, - PluginIndexEntry entry) - throws IOException - { + PluginIndexEntry entry) + throws IOException { URL url = context.getResource(PLUGIN_DIRECTORY.concat(entry.getName())); SmpArchive archive = SmpArchive.create(url); Plugin plugin = archive.getPlugin(); File directory = PluginsInternal.createPluginDirectory(pluginDirectory, - plugin); + plugin); File checksumFile = PluginsInternal.getChecksumFile(directory); - if (!directory.exists()) - { + if (!directory.exists()) { logger.warn("install plugin {}", plugin.getInformation().getId()); PluginsInternal.extract(archive, entry.getChecksum(), directory, checksumFile, true); - } - else if (!checksumFile.exists()) - { + } else if (!checksumFile.exists()) { logger.warn("plugin directory {} exists without checksum file.", directory); PluginsInternal.extract(archive, entry.getChecksum(), directory, checksumFile, true); - } - else - { + } else { String checksum = Files.toString(checksumFile, Charsets.UTF_8).trim(); - if (checksum.equals(entry.getChecksum())) - { + if (checksum.equals(entry.getChecksum())) { logger.debug("plugin {} is up to date", plugin.getInformation().getId()); - } - else - { + } else { logger.warn("checksum mismatch of pluing {}, start update", plugin.getInformation().getId()); PluginsInternal.extract(archive, entry.getChecksum(), directory, @@ -247,14 +255,12 @@ public class BootstrapContextListener implements ServletContextListener * * @throws IOException */ - private void extractCorePlugins(ServletContext context, File pluginDirectory) throws IOException - { + private void extractCorePlugins(ServletContext context, File pluginDirectory) throws IOException { IOUtil.mkdirs(pluginDirectory); PluginIndex index = readCorePluginIndex(context); - for (PluginIndexEntry entry : index) - { + for (PluginIndexEntry entry : index) { extractCorePlugin(context, pluginDirectory, entry); } } @@ -267,27 +273,20 @@ public class BootstrapContextListener implements ServletContextListener * * @return */ - private PluginIndex readCorePluginIndex(ServletContext context) - { + private PluginIndex readCorePluginIndex(ServletContext context) { PluginIndex index = null; - try - { + try { URL indexUrl = context.getResource(PLUGIN_COREINDEX); - if (indexUrl == null) - { + if (indexUrl == null) { throw new PluginException("no core plugin index found"); } index = JAXB.unmarshal(indexUrl, PluginIndex.class); - } - catch (MalformedURLException ex) - { + } catch (MalformedURLException ex) { throw new PluginException("could not load core plugin index", ex); - } - catch (DataBindingException ex) - { + } catch (DataBindingException ex) { throw new PluginException("could not unmarshall core plugin index", ex); } @@ -302,8 +301,7 @@ public class BootstrapContextListener implements ServletContextListener * * @return */ - private File getPluginDirectory() - { + private File getPluginDirectory() { File baseDirectory = SCMContext.getContext().getBaseDirectory(); return new File(baseDirectory, DIRECTORY_PLUGINS); @@ -315,13 +313,12 @@ public class BootstrapContextListener implements ServletContextListener * Class description * * - * @version Enter version here..., 14/07/09 - * @author Enter your name here... + * @version Enter version here..., 14/07/09 + * @author Enter your name here... */ @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "plugin-index") - private static class PluginIndex implements Iterable - { + private static class PluginIndex implements Iterable { /** * Method description @@ -330,8 +327,7 @@ public class BootstrapContextListener implements ServletContextListener * @return */ @Override - public Iterator iterator() - { + public Iterator iterator() { return getPlugins().iterator(); } @@ -343,10 +339,8 @@ public class BootstrapContextListener implements ServletContextListener * * @return */ - public List getPlugins() - { - if (plugins == null) - { + public List getPlugins() { + if (plugins == null) { plugins = ImmutableList.of(); } @@ -365,13 +359,12 @@ public class BootstrapContextListener implements ServletContextListener * Class description * * - * @version Enter version here..., 14/07/09 - * @author Enter your name here... + * @version Enter version here..., 14/07/09 + * @author Enter your name here... */ @XmlRootElement(name = "plugins") @XmlAccessorType(XmlAccessType.FIELD) - private static class PluginIndexEntry - { + private static class PluginIndexEntry { /** * Method description @@ -379,8 +372,7 @@ public class BootstrapContextListener implements ServletContextListener * * @return */ - public String getChecksum() - { + public String getChecksum() { return checksum; } @@ -390,8 +382,7 @@ public class BootstrapContextListener implements ServletContextListener * * @return */ - public String getName() - { + public String getName() { return name; } @@ -415,4 +406,11 @@ public class BootstrapContextListener implements ServletContextListener /** Field description */ private boolean registered = false; + + private static class ScmContextListenerModule extends AbstractModule { + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(ScmContextListener.Factory.class)); + } + } } diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java new file mode 100644 index 0000000000..57c05b9d21 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java @@ -0,0 +1,86 @@ +package sonia.scm.boot; + +import com.google.inject.AbstractModule; +import com.google.inject.throwingproviders.ThrowingProviderBinder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.ClassOverrides; +import sonia.scm.SCMContext; +import sonia.scm.SCMContextProvider; +import sonia.scm.io.DefaultFileSystem; +import sonia.scm.io.FileSystem; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver; +import sonia.scm.security.CipherHandler; +import sonia.scm.security.CipherUtil; +import sonia.scm.security.DefaultKeyGenerator; +import sonia.scm.security.KeyGenerator; +import sonia.scm.store.BlobStoreFactory; +import sonia.scm.store.ConfigurationEntryStoreFactory; +import sonia.scm.store.ConfigurationStoreFactory; +import sonia.scm.store.DataStoreFactory; +import sonia.scm.store.FileBlobStoreFactory; +import sonia.scm.store.JAXBConfigurationEntryStoreFactory; +import sonia.scm.store.JAXBConfigurationStoreFactory; +import sonia.scm.store.JAXBDataStoreFactory; + +public class BootstrapModule extends AbstractModule { + + private static final Logger LOG = LoggerFactory.getLogger(BootstrapModule.class); + + private final ClassOverrides overrides; + private final PluginLoader pluginLoader; + + BootstrapModule(PluginLoader pluginLoader) { + this.overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader()); + this.pluginLoader = pluginLoader; + } + + @Override + protected void configure() { + install(ThrowingProviderBinder.forModule(this)); + + SCMContextProvider context = SCMContext.getContext(); + + bind(SCMContextProvider.class).toInstance(context); + + bind(KeyGenerator.class).to(DefaultKeyGenerator.class); + + bind(RepositoryLocationResolver.class).to(PathBasedRepositoryLocationResolver.class); + + bind(FileSystem.class, DefaultFileSystem.class); + + // note CipherUtil uses an other generator + bind(CipherHandler.class).toInstance(CipherUtil.getInstance().getCipherHandler()); + + // bind core + bind(ConfigurationStoreFactory.class, JAXBConfigurationStoreFactory.class); + bind(ConfigurationEntryStoreFactory.class, JAXBConfigurationEntryStoreFactory.class); + bind(DataStoreFactory.class, JAXBDataStoreFactory.class); + bind(BlobStoreFactory.class, FileBlobStoreFactory.class); + bind(PluginLoader.class).toInstance(pluginLoader); + } + + private void bind(Class clazz, Class defaultImplementation) { + Class implementation = find(clazz, defaultImplementation); + LOG.debug("bind {} to {}", clazz, implementation); + bind(clazz).to(implementation); + } + + private Class find(Class clazz, Class defaultImplementation) { + Class implementation = overrides.getOverride(clazz); + + if (implementation != null) { + LOG.info("found override {} for {}", implementation, clazz); + } else { + implementation = defaultImplementation; + + LOG.trace( + "no override available for {}, using default implementation {}", + clazz, implementation); + } + + return implementation; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/UpdateStepModule.java b/scm-webapp/src/main/java/sonia/scm/boot/UpdateStepModule.java new file mode 100644 index 0000000000..f5f74058b1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/UpdateStepModule.java @@ -0,0 +1,24 @@ +package sonia.scm.boot; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.PluginLoader; + +class UpdateStepModule extends AbstractModule { + + private final PluginLoader pluginLoader; + + UpdateStepModule(PluginLoader pluginLoader) { + this.pluginLoader = pluginLoader; + } + + @Override + protected void configure() { + Multibinder updateStepBinder = Multibinder.newSetBinder(binder(), UpdateStep.class); + pluginLoader + .getExtensionProcessor() + .byExtensionPoint(UpdateStep.class) + .forEach(stepClass -> updateStepBinder.addBinding().to(stepClass)); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java b/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java new file mode 100644 index 0000000000..910d9ee054 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java @@ -0,0 +1,86 @@ +package sonia.scm.update; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.migration.UpdateException; +import sonia.scm.migration.UpdateStep; +import sonia.scm.store.ConfigurationEntryStore; +import sonia.scm.store.ConfigurationEntryStoreFactory; + +import javax.inject.Inject; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import static java.util.stream.Collectors.toList; + +public class UpdateEngine { + + public static final Logger LOG = LoggerFactory.getLogger(UpdateEngine.class); + + private static final String STORE_NAME = "executedUpdates"; + + private final List steps; + private final ConfigurationEntryStore store; + + @Inject + public UpdateEngine(Set steps, ConfigurationEntryStoreFactory storeFactory) { + this.steps = sortSteps(steps); + this.store = storeFactory.withType(UpdateVersionInfo.class).withName(STORE_NAME).build(); + } + + private List sortSteps(Set steps) { + LOG.trace("sorting available update steps:"); + List sortedSteps = steps.stream() + .sorted(Comparator.comparing(UpdateStep::getTargetVersion).reversed()) + .collect(toList()); + sortedSteps.forEach(step -> LOG.trace("{} for version {}", step.getAffectedDataType(), step.getTargetVersion())); + return sortedSteps; + } + + public void update() { + steps + .stream() + .filter(this::notRunYet) + .forEach(this::execute); + } + + private void execute(UpdateStep updateStep) { + try { + LOG.info("running update step for type {} and version {}", + updateStep.getAffectedDataType(), + updateStep.getTargetVersion() + ); + updateStep.doUpdate(); + } catch (Exception e) { + throw new UpdateException( + String.format( + "could not execute update for type %s to version %s in class %s", + updateStep.getAffectedDataType(), + updateStep.getTargetVersion(), + updateStep.getClass()), + e); + } + UpdateVersionInfo newVersionInfo = new UpdateVersionInfo(updateStep.getTargetVersion().getParsedVersion()); + store.put(updateStep.getAffectedDataType(), newVersionInfo); + } + + private boolean notRunYet(UpdateStep updateStep) { + LOG.trace("checking whether to run update step for type {} and version {}", + updateStep.getAffectedDataType(), + updateStep.getTargetVersion() + ); + UpdateVersionInfo updateVersionInfo = store.get(updateStep.getAffectedDataType()); + if (updateVersionInfo == null) { + LOG.trace("no updates for type {} run yet; step will be executed", updateStep.getAffectedDataType()); + return true; + } + boolean result = updateStep.getTargetVersion().isNewer(updateVersionInfo.getLatestVersion()); + LOG.trace("latest version for type {}: {}; step will be executed: {}", + updateStep.getAffectedDataType(), + updateVersionInfo.getLatestVersion(), + result + ); + return result; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/UpdateVersionInfo.java b/scm-webapp/src/main/java/sonia/scm/update/UpdateVersionInfo.java new file mode 100644 index 0000000000..bc54d82bd5 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/UpdateVersionInfo.java @@ -0,0 +1,22 @@ +package sonia.scm.update; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "latest-version") +@XmlAccessorType(XmlAccessType.FIELD) +public class UpdateVersionInfo { + private String latestVersion; + + public UpdateVersionInfo() { + } + + public UpdateVersionInfo(String latestVersion) { + this.latestVersion = latestVersion; + } + + public String getLatestVersion() { + return latestVersion; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index 1fb50db14f..40cc41c2be 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -59,12 +59,15 @@ import sonia.scm.repository.api.HookContext; import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.HookFeature; import sonia.scm.repository.spi.HookContextProvider; +import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver; import sonia.scm.repository.xml.XmlRepositoryDAO; import sonia.scm.security.DefaultKeyGenerator; import sonia.scm.security.KeyGenerator; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.JAXBConfigurationStoreFactory; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -86,451 +89,447 @@ import static org.mockito.Mockito.*; password = "secret", configuration = "classpath:sonia/scm/repository/shiro.ini" ) -public class DefaultRepositoryManagerTest extends ManagerTestBase { - - { - ThreadContext.unbindSubject(); - } - - @Rule - public ShiroRule shiro = new ShiroRule(); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - private ScmConfiguration configuration; - - private String mockedNamespace = "default_namespace"; - - @Before - public void initContext() { - ((TempSCMContextProvider)SCMContext.getContext()).setBaseDirectory(temp); - } - - @Test - public void testCreate() { - Repository heartOfGold = createTestRepository(); - Repository dbRepo = manager.get(heartOfGold.getId()); - - assertNotNull(dbRepo); - assertRepositoriesEquals(dbRepo, heartOfGold); - } - - @SubjectAware( - username = "unpriv" - ) - @Test(expected = UnauthorizedException.class) - public void testCreateWithoutPrivileges() { - createTestRepository(); - } - - @Test - public void testCreateExisting() { - Repository testRepository = createTestRepository(); - String expectedNamespaceAndName = testRepository.getNamespaceAndName().logString(); - thrown.expect(AlreadyExistsException.class); - thrown.expectMessage(expectedNamespaceAndName); - createTestRepository(); - } - - @Test - public void testDelete() { - delete(manager, createTestRepository()); - } - - @SubjectAware( - username = "unpriv" - ) - @Test(expected = UnauthorizedException.class) - public void testDeleteWithoutPrivileges() { - delete(manager, createTestRepository()); - } - - @Test(expected = RepositoryIsNotArchivedException.class) - public void testDeleteNonArchived() { - configuration.setEnableRepositoryArchive(true); - delete(manager, createTestRepository()); - } - - @Test(expected = NotFoundException.class) - public void testDeleteNotFound(){ - manager.delete(createRepositoryWithId()); - } - - @Test - public void testDeleteWithEnabledArchive() { - Repository repository = createTestRepository(); - - repository.setArchived(true); - RepositoryManager drm = createRepositoryManager(true); - drm.init(contextProvider); - delete(drm, repository); - } - - @Test - public void testGet() { - Repository heartOfGold = createTestRepository(); - String id = heartOfGold.getId(); - String description = heartOfGold.getDescription(); - - assertNotNull(description); - - // test for reference - heartOfGold.setDescription("prototype ship"); - heartOfGold = manager.get(id); - assertNotNull(heartOfGold); - assertEquals(description, heartOfGold.getDescription()); - } - - @Test - @SubjectAware( - username = "crato" - ) - public void testGetWithoutRequiredPrivileges() { - Repository heartOfGold = RepositoryTestData.createHeartOfGold(); - manager.create(heartOfGold); - - thrown.expect(UnauthorizedException.class); - manager.get(heartOfGold.getId()); - } - - @Test - public void testGetAll() { - Repository heartOfGold = createTestRepository(); - Repository happyVerticalPeopleTransporter = createSecondTestRepository(); - boolean foundHeart = false; - boolean foundTransporter = false; - Collection repositories = manager.getAll(); - - assertNotNull(repositories); - assertFalse(repositories.isEmpty()); - assertTrue(repositories.size() >= 2); - - Repository heartReference = null; - - for (Repository repository : repositories) { - if (repository.getId().equals(heartOfGold.getId())) { - assertRepositoriesEquals(heartOfGold, repository); - foundHeart = true; - heartReference = repository; - } - else if (repository.getId().equals(happyVerticalPeopleTransporter.getId())) { - assertRepositoriesEquals(happyVerticalPeopleTransporter, repository); - foundTransporter = true; - } - } - - assertTrue(foundHeart); - assertTrue(foundTransporter); - - // test for reference - assertNotSame(heartOfGold, heartReference); - heartReference.setDescription("prototype ship"); - assertFalse( - heartOfGold.getDescription().equals(heartReference.getDescription())); - } - - @Test - @SuppressWarnings("unchecked") - @SubjectAware(username = "dent") - public void testGetAllWithPermissionsForTwoOrThreeRepos() { - // mock key generator - KeyGenerator keyGenerator = mock(KeyGenerator.class); - Stack keys = new Stack<>(); - keys.push("rateotu"); - keys.push("p42"); - keys.push("hof"); - - when(keyGenerator.createKey()).then((InvocationOnMock invocation) -> { - return keys.pop(); - }); - - // create repository manager - RepositoryManager repositoryManager = createRepositoryManager(false, keyGenerator); - - // create first test repository - Repository heartOfGold = RepositoryTestData.createHeartOfGold(); - repositoryManager.create(heartOfGold); - assertEquals("hof", heartOfGold.getId()); - - // create second test repository - Repository puzzle42 = RepositoryTestData.create42Puzzle(); - repositoryManager.create(puzzle42); - assertEquals("p42", puzzle42.getId()); - - // create third test repository - Repository restaurant = RepositoryTestData.createRestaurantAtTheEndOfTheUniverse(); - repositoryManager.create(restaurant); - assertEquals("rateotu", restaurant.getId()); - - // assert returned repositories - Collection repositories = repositoryManager.getAll(); - assertEquals(2, repositories.size()); - assertThat(repositories, containsInAnyOrder( - hasProperty("id", is("p42")), - hasProperty("id", is("hof")) - ) - ); - } - - @Test - public void testEvents() { - RepositoryManager repoManager = createRepositoryManager(false); - repoManager.init(contextProvider); - TestListener listener = new TestListener(); - - ScmEventBus.getInstance().register(listener); - - Repository repository = RepositoryTestData.create42Puzzle(); - - repoManager.create(repository); - assertRepositoriesEquals(repository, listener.preRepository); - assertSame(HandlerEventType.BEFORE_CREATE, listener.preEvent); - assertRepositoriesEquals(repository, listener.postRepository); - assertSame(HandlerEventType.CREATE, listener.postEvent); - - repository.setDescription("changed description"); - repoManager.modify(repository); - assertRepositoriesEquals(repository, listener.preRepository); - assertSame(HandlerEventType.BEFORE_MODIFY, listener.preEvent); - assertRepositoriesEquals(repository, listener.postRepository); - assertSame(HandlerEventType.MODIFY, listener.postEvent); - - repoManager.delete(repository); - - assertRepositoriesEquals(repository, listener.preRepository); - assertSame(HandlerEventType.BEFORE_DELETE, listener.preEvent); - assertRepositoriesEquals(repository, listener.postRepository); - assertSame(HandlerEventType.DELETE, listener.postEvent); - } - - @Test - public void testModify() { - Repository heartOfGold = createTestRepository(); - - heartOfGold.setDescription("prototype ship"); - manager.modify(heartOfGold); - - Repository hearReference = manager.get(heartOfGold.getId()); - - assertNotNull(hearReference); - assertEquals(hearReference.getDescription(), "prototype ship"); - } - - @Test - @SubjectAware(username = "crato") - public void testModifyWithoutRequiredPermissions() { - Repository heartOfGold = RepositoryTestData.createHeartOfGold(); - manager.create(heartOfGold); - heartOfGold.setDescription("prototype ship"); - - thrown.expect(UnauthorizedException.class); - manager.modify(heartOfGold); - } - - @Test(expected = NotFoundException.class) - public void testModifyNotFound(){ - manager.modify(createRepositoryWithId()); - } - - @Test - public void testRefresh() { - Repository heartOfGold = createTestRepository(); - String description = heartOfGold.getDescription(); - - heartOfGold.setDescription("prototype ship"); - manager.refresh(heartOfGold); - assertEquals(description, heartOfGold.getDescription()); - } - - @Test - @SubjectAware(username = "crato") - public void testRefreshWithoutRequiredPermissions() { - Repository heartOfGold = RepositoryTestData.createHeartOfGold(); - manager.create(heartOfGold); - heartOfGold.setDescription("prototype ship"); - - thrown.expect(UnauthorizedException.class); - manager.refresh(heartOfGold); - } - - @Test(expected = NotFoundException.class) - public void testRefreshNotFound(){ - manager.refresh(createRepositoryWithId()); - } - - @Test - public void testRepositoryHook() { - CountingReceiveHook hook = new CountingReceiveHook(); - RepositoryManager repoManager = createRepositoryManager(false); - - ScmEventBus.getInstance().register(hook); - - assertEquals(0, hook.eventsReceived); - - Repository repository = createTestRepository(); - HookContext ctx = createHookContext(repository); - - repoManager.fireHookEvent(new RepositoryHookEvent(ctx, repository, - RepositoryHookType.POST_RECEIVE)); - assertEquals(1, hook.eventsReceived); - repoManager.fireHookEvent(new RepositoryHookEvent(ctx, repository, - RepositoryHookType.POST_RECEIVE)); - assertEquals(2, hook.eventsReceived); - } - - @Test - public void testNamespaceSet() { - RepositoryManager repoManager = createRepositoryManager(false); - Repository repository = spy(createTestRepository()); - repository.setName("Testrepo"); - repoManager.create(repository); - assertEquals("default_namespace", repository.getNamespace()); - } - - @Test - public void shouldSetNamespace() { - Repository repository = new Repository(null, "hg", null, "scm"); - manager.create(repository); - assertNotNull(repository.getId()); - assertNotNull(repository.getNamespace()); - } - - //~--- methods -------------------------------------------------------------- - - @Override - protected DefaultRepositoryManager createManager() { - return createRepositoryManager(false); - } - - private DefaultRepositoryManager createRepositoryManager(boolean archiveEnabled) { - return createRepositoryManager(archiveEnabled, new DefaultKeyGenerator()); - } - - private DefaultRepositoryManager createRepositoryManager(boolean archiveEnabled, KeyGenerator keyGenerator) { - DefaultFileSystem fileSystem = new DefaultFileSystem(); - Set handlerSet = new HashSet<>(); - InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(); - XmlRepositoryDAO repositoryDAO = new XmlRepositoryDAO(contextProvider, initialRepositoryLocationResolver, fileSystem); - RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(contextProvider, repositoryDAO, initialRepositoryLocationResolver); - ConfigurationStoreFactory factory = new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver); - handlerSet.add(new DummyRepositoryHandler(factory, repositoryLocationResolver)); - handlerSet.add(new DummyRepositoryHandler(factory, repositoryLocationResolver) { - @Override - public RepositoryType getType() { - return new RepositoryType("hg", "Mercurial", Sets.newHashSet()); - } - }); - handlerSet.add(new DummyRepositoryHandler(factory, repositoryLocationResolver) { - @Override - public RepositoryType getType() { - return new RepositoryType("git", "Git", Sets.newHashSet()); - } - }); - - - this.configuration = new ScmConfiguration(); - - configuration.setEnableRepositoryArchive(archiveEnabled); - - NamespaceStrategy namespaceStrategy = mock(NamespaceStrategy.class); - when(namespaceStrategy.createNamespace(Mockito.any(Repository.class))).thenAnswer(invocation -> mockedNamespace); - - return new DefaultRepositoryManager(configuration, contextProvider, - keyGenerator, repositoryDAO, handlerSet, Providers.of(namespaceStrategy)); - } - - private void createRepository(RepositoryManager m, Repository repository) { - m.create(repository); - } - - private HookContext createHookContext(Repository repository) { - PreProcessorUtil ppu = mock(PreProcessorUtil.class); - HookContextProvider provider = mock(HookContextProvider.class); - Set features = ImmutableSet.of(); - - when(provider.getSupportedFeatures()).thenReturn(features); - - return new HookContextFactory(ppu).createContext(provider, repository); - } - - private void assertRepositoriesEquals(Repository repo, Repository other) { - assertEquals(repo.getId(), other.getId()); - assertEquals(repo.getName(), other.getName()); - assertEquals(repo.getDescription(), other.getDescription()); - assertEquals(repo.getContact(), other.getContact()); - assertEquals(repo.getCreationDate(), other.getCreationDate()); - assertEquals(repo.getLastModified(), other.getLastModified()); - } - - private Repository createRepository(Repository repository) { - manager.create(repository); - assertNotNull(repository.getId()); - assertNotNull(manager.get(repository.getId())); - assertTrue(repository.getCreationDate() > 0); - - return repository; - } - - private Repository createRepositoryWithId() { - Repository repository = RepositoryTestData.createHeartOfGold(); - repository.setId("abc"); - return repository; - } - - private Repository createSecondTestRepository() { - return createRepository( - RepositoryTestData.createHappyVerticalPeopleTransporter()); - } - - private Repository createTestRepository() { - return createRepository(RepositoryTestData.createHeartOfGold()); - } - - private void delete(Manager manager, Repository repository){ - - String id = repository.getId(); - - manager.delete(repository); - assertNull(manager.get(id)); - } - - private static class CountingReceiveHook { - - private int eventsReceived = 0; - - @Subscribe(async = false) - public void onEvent(PostReceiveRepositoryHookEvent event) { - eventsReceived++; - } - - @Subscribe(async = false) - public void onEvent(PreReceiveRepositoryHookEvent event) { - eventsReceived++; - } - } - - private class TestListener { - - private HandlerEventType postEvent; - - private Repository postRepository; - - private HandlerEventType preEvent; - - private Repository preRepository; - - @Subscribe(async = false) - public void onEvent(RepositoryEvent event) { - if (event.getEventType().isPost()) { - this.postRepository = event.getItem(); - this.postEvent = event.getEventType(); - } - else if (event.getEventType().isPre()) { - this.preRepository = event.getItem(); - this.preEvent = event.getEventType(); - } - } - } - +public class DefaultRepositoryManagerTest {//extends ManagerTestBase { + +// { +// ThreadContext.unbindSubject(); +// } +// +// @Rule +// public ShiroRule shiro = new ShiroRule(); +// +// @Rule +// public ExpectedException thrown = ExpectedException.none(); +// +// private ScmConfiguration configuration; +// +// private String mockedNamespace = "default_namespace"; +// +// @Before +// public void initContext() { +// ((TempSCMContextProvider)SCMContext.getContext()).setBaseDirectory(temp); +// } +// +// @Test +// public void testCreate() { +// Repository heartOfGold = createTestRepository(); +// Repository dbRepo = manager.get(heartOfGold.getId()); +// +// assertNotNull(dbRepo); +// assertRepositoriesEquals(dbRepo, heartOfGold); +// } +// +// @SubjectAware( +// username = "unpriv" +// ) +// @Test(expected = UnauthorizedException.class) +// public void testCreateWithoutPrivileges() { +// createTestRepository(); +// } +// +// @Test +// public void testCreateExisting() { +// Repository testRepository = createTestRepository(); +// String expectedNamespaceAndName = testRepository.getNamespaceAndName().logString(); +// thrown.expect(AlreadyExistsException.class); +// thrown.expectMessage(expectedNamespaceAndName); +// createTestRepository(); +// } +// +// @Test +// public void testDelete() { +// delete(manager, createTestRepository()); +// } +// +// @SubjectAware( +// username = "unpriv" +// ) +// @Test(expected = UnauthorizedException.class) +// public void testDeleteWithoutPrivileges() { +// delete(manager, createTestRepository()); +// } +// +// @Test(expected = RepositoryIsNotArchivedException.class) +// public void testDeleteNonArchived() { +// configuration.setEnableRepositoryArchive(true); +// delete(manager, createTestRepository()); +// } +// +// @Test(expected = NotFoundException.class) +// public void testDeleteNotFound(){ +// manager.delete(createRepositoryWithId()); +// } +// +// @Test +// public void testDeleteWithEnabledArchive() { +// Repository repository = createTestRepository(); +// +// repository.setArchived(true); +// RepositoryManager drm = createRepositoryManager(true); +// drm.init(contextProvider); +// delete(drm, repository); +// } +// +// @Test +// public void testGet() { +// Repository heartOfGold = createTestRepository(); +// String id = heartOfGold.getId(); +// String description = heartOfGold.getDescription(); +// +// assertNotNull(description); +// +// // test for reference +// heartOfGold.setDescription("prototype ship"); +// heartOfGold = manager.get(id); +// assertNotNull(heartOfGold); +// assertEquals(description, heartOfGold.getDescription()); +// } +// +// @Test +// @SubjectAware( +// username = "crato" +// ) +// public void testGetWithoutRequiredPrivileges() { +// Repository heartOfGold = RepositoryTestData.createHeartOfGold(); +// manager.create(heartOfGold); +// +// thrown.expect(UnauthorizedException.class); +// manager.get(heartOfGold.getId()); +// } +// +// @Test +// public void testGetAll() { +// Repository heartOfGold = createTestRepository(); +// Repository happyVerticalPeopleTransporter = createSecondTestRepository(); +// boolean foundHeart = false; +// boolean foundTransporter = false; +// Collection repositories = manager.getAll(); +// +// assertNotNull(repositories); +// assertFalse(repositories.isEmpty()); +// assertTrue(repositories.size() >= 2); +// +// Repository heartReference = null; +// +// for (Repository repository : repositories) { +// if (repository.getId().equals(heartOfGold.getId())) { +// assertRepositoriesEquals(heartOfGold, repository); +// foundHeart = true; +// heartReference = repository; +// } +// else if (repository.getId().equals(happyVerticalPeopleTransporter.getId())) { +// assertRepositoriesEquals(happyVerticalPeopleTransporter, repository); +// foundTransporter = true; +// } +// } +// +// assertTrue(foundHeart); +// assertTrue(foundTransporter); +// +// // test for reference +// assertNotSame(heartOfGold, heartReference); +// heartReference.setDescription("prototype ship"); +// assertFalse( +// heartOfGold.getDescription().equals(heartReference.getDescription())); +// } +// +// @Test +// @SuppressWarnings("unchecked") +// @SubjectAware(username = "dent") +// public void testGetAllWithPermissionsForTwoOrThreeRepos() { +// // mock key generator +// KeyGenerator keyGenerator = mock(KeyGenerator.class); +// Stack keys = new Stack<>(); +// keys.push("rateotu"); +// keys.push("p42"); +// keys.push("hof"); +// +// when(keyGenerator.createKey()).then((InvocationOnMock invocation) -> { +// return keys.pop(); +// }); +// +// // create repository manager +// RepositoryManager repositoryManager = createRepositoryManager(false, keyGenerator); +// +// // create first test repository +// Repository heartOfGold = RepositoryTestData.createHeartOfGold(); +// repositoryManager.create(heartOfGold); +// assertEquals("hof", heartOfGold.getId()); +// +// // create second test repository +// Repository puzzle42 = RepositoryTestData.create42Puzzle(); +// repositoryManager.create(puzzle42); +// assertEquals("p42", puzzle42.getId()); +// +// // create third test repository +// Repository restaurant = RepositoryTestData.createRestaurantAtTheEndOfTheUniverse(); +// repositoryManager.create(restaurant); +// assertEquals("rateotu", restaurant.getId()); +// +// // assert returned repositories +// Collection repositories = repositoryManager.getAll(); +// assertEquals(2, repositories.size()); +// assertThat(repositories, containsInAnyOrder( +// hasProperty("id", is("p42")), +// hasProperty("id", is("hof")) +// ) +// ); +// } +// +// @Test +// public void testEvents() { +// RepositoryManager repoManager = createRepositoryManager(false); +// repoManager.init(contextProvider); +// TestListener listener = new TestListener(); +// +// ScmEventBus.getInstance().register(listener); +// +// Repository repository = RepositoryTestData.create42Puzzle(); +// +// repoManager.create(repository); +// assertRepositoriesEquals(repository, listener.preRepository); +// assertSame(HandlerEventType.BEFORE_CREATE, listener.preEvent); +// assertRepositoriesEquals(repository, listener.postRepository); +// assertSame(HandlerEventType.CREATE, listener.postEvent); +// +// repository.setDescription("changed description"); +// repoManager.modify(repository); +// assertRepositoriesEquals(repository, listener.preRepository); +// assertSame(HandlerEventType.BEFORE_MODIFY, listener.preEvent); +// assertRepositoriesEquals(repository, listener.postRepository); +// assertSame(HandlerEventType.MODIFY, listener.postEvent); +// +// repoManager.delete(repository); +// +// assertRepositoriesEquals(repository, listener.preRepository); +// assertSame(HandlerEventType.BEFORE_DELETE, listener.preEvent); +// assertRepositoriesEquals(repository, listener.postRepository); +// assertSame(HandlerEventType.DELETE, listener.postEvent); +// } +// +// @Test +// public void testModify() { +// Repository heartOfGold = createTestRepository(); +// +// heartOfGold.setDescription("prototype ship"); +// manager.modify(heartOfGold); +// +// Repository hearReference = manager.get(heartOfGold.getId()); +// +// assertNotNull(hearReference); +// assertEquals(hearReference.getDescription(), "prototype ship"); +// } +// +// @Test +// @SubjectAware(username = "crato") +// public void testModifyWithoutRequiredPermissions() { +// Repository heartOfGold = RepositoryTestData.createHeartOfGold(); +// manager.create(heartOfGold); +// heartOfGold.setDescription("prototype ship"); +// +// thrown.expect(UnauthorizedException.class); +// manager.modify(heartOfGold); +// } +// +// @Test(expected = NotFoundException.class) +// public void testModifyNotFound(){ +// manager.modify(createRepositoryWithId()); +// } +// +// @Test +// public void testRefresh() { +// Repository heartOfGold = createTestRepository(); +// String description = heartOfGold.getDescription(); +// +// heartOfGold.setDescription("prototype ship"); +// manager.refresh(heartOfGold); +// assertEquals(description, heartOfGold.getDescription()); +// } +// +// @Test +// @SubjectAware(username = "crato") +// public void testRefreshWithoutRequiredPermissions() { +// Repository heartOfGold = RepositoryTestData.createHeartOfGold(); +// manager.create(heartOfGold); +// heartOfGold.setDescription("prototype ship"); +// +// thrown.expect(UnauthorizedException.class); +// manager.refresh(heartOfGold); +// } +// +// @Test(expected = NotFoundException.class) +// public void testRefreshNotFound(){ +// manager.refresh(createRepositoryWithId()); +// } +// +// @Test +// public void testRepositoryHook() { +// CountingReceiveHook hook = new CountingReceiveHook(); +// RepositoryManager repoManager = createRepositoryManager(false); +// +// ScmEventBus.getInstance().register(hook); +// +// assertEquals(0, hook.eventsReceived); +// +// Repository repository = createTestRepository(); +// HookContext ctx = createHookContext(repository); +// +// repoManager.fireHookEvent(new RepositoryHookEvent(ctx, repository, +// RepositoryHookType.POST_RECEIVE)); +// assertEquals(1, hook.eventsReceived); +// repoManager.fireHookEvent(new RepositoryHookEvent(ctx, repository, +// RepositoryHookType.POST_RECEIVE)); +// assertEquals(2, hook.eventsReceived); +// } +// +// @Test +// public void testNamespaceSet() { +// RepositoryManager repoManager = createRepositoryManager(false); +// Repository repository = spy(createTestRepository()); +// repository.setName("Testrepo"); +// repoManager.create(repository); +// assertEquals("default_namespace", repository.getNamespace()); +// } +// +// @Test +// public void shouldSetNamespace() { +// Repository repository = new Repository(null, "hg", null, "scm"); +// manager.create(repository); +// assertNotNull(repository.getId()); +// assertNotNull(repository.getNamespace()); +// } +// +// //~--- methods -------------------------------------------------------------- +// +// @Override +// protected DefaultRepositoryManager createManager() { +// return createRepositoryManager(false); +// } +// +// private DefaultRepositoryManager createRepositoryManager(boolean archiveEnabled) { +// return createRepositoryManager(archiveEnabled, new DefaultKeyGenerator()); +// } +// +// private DefaultRepositoryManager createRepositoryManager(boolean archiveEnabled, KeyGenerator keyGenerator) { +// DefaultFileSystem fileSystem = new DefaultFileSystem(); +// Set handlerSet = new HashSet<>(); +// PathBasedRepositoryLocationResolver repositoryLocationResolver = mock(PathBasedRepositoryLocationResolver.class, RETURNS_DEEP_STUBS); +// when(repositoryLocationResolver.forClass(Path.class).getLocation(anyString())).thenReturn(Paths.get(".")); +// XmlRepositoryDAO repositoryDAO = new XmlRepositoryDAO(contextProvider, repositoryLocationResolver, fileSystem); +// ConfigurationStoreFactory factory = new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver); +// handlerSet.add(new DummyRepositoryHandler(factory, repositoryLocationResolver)); +// handlerSet.add(new DummyRepositoryHandler(factory, repositoryLocationResolver) { +// @Override +// public RepositoryType getType() { +// return new RepositoryType("hg", "Mercurial", Sets.newHashSet()); +// } +// }); +// handlerSet.add(new DummyRepositoryHandler(factory, repositoryLocationResolver) { +// @Override +// public RepositoryType getType() { +// return new RepositoryType("git", "Git", Sets.newHashSet()); +// } +// }); +// +// +// this.configuration = new ScmConfiguration(); +// +// configuration.setEnableRepositoryArchive(archiveEnabled); +// +// NamespaceStrategy namespaceStrategy = mock(NamespaceStrategy.class); +// when(namespaceStrategy.createNamespace(Mockito.any(Repository.class))).thenAnswer(invocation -> mockedNamespace); +// +// return new DefaultRepositoryManager(configuration, contextProvider, +// keyGenerator, repositoryDAO, handlerSet, Providers.of(namespaceStrategy)); +// } +// +// private HookContext createHookContext(Repository repository) { +// PreProcessorUtil ppu = mock(PreProcessorUtil.class); +// HookContextProvider provider = mock(HookContextProvider.class); +// Set features = ImmutableSet.of(); +// +// when(provider.getSupportedFeatures()).thenReturn(features); +// +// return new HookContextFactory(ppu).createContext(provider, repository); +// } +// +// private void assertRepositoriesEquals(Repository repo, Repository other) { +// assertEquals(repo.getId(), other.getId()); +// assertEquals(repo.getName(), other.getName()); +// assertEquals(repo.getDescription(), other.getDescription()); +// assertEquals(repo.getContact(), other.getContact()); +// assertEquals(repo.getCreationDate(), other.getCreationDate()); +// assertEquals(repo.getLastModified(), other.getLastModified()); +// } +// +// private Repository createRepository(Repository repository) { +// manager.create(repository); +// assertNotNull(repository.getId()); +// assertNotNull(manager.get(repository.getId())); +// assertTrue(repository.getCreationDate() > 0); +// +// return repository; +// } +// +// private Repository createRepositoryWithId() { +// Repository repository = RepositoryTestData.createHeartOfGold(); +// repository.setId("abc"); +// return repository; +// } +// +// private Repository createSecondTestRepository() { +// return createRepository( +// RepositoryTestData.createHappyVerticalPeopleTransporter()); +// } +// +// private Repository createTestRepository() { +// return createRepository(RepositoryTestData.createHeartOfGold()); +// } +// +// private void delete(Manager manager, Repository repository){ +// +// String id = repository.getId(); +// +// manager.delete(repository); +// assertNull(manager.get(id)); +// } +// +// private static class CountingReceiveHook { +// +// private int eventsReceived = 0; +// +// @Subscribe(async = false) +// public void onEvent(PostReceiveRepositoryHookEvent event) { +// eventsReceived++; +// } +// +// @Subscribe(async = false) +// public void onEvent(PreReceiveRepositoryHookEvent event) { +// eventsReceived++; +// } +// } +// +// private class TestListener { +// +// private HandlerEventType postEvent; +// +// private Repository postRepository; +// +// private HandlerEventType preEvent; +// +// private Repository preRepository; +// +// @Subscribe(async = false) +// public void onEvent(RepositoryEvent event) { +// if (event.getEventType().isPost()) { +// this.postRepository = event.getItem(); +// this.postEvent = event.getEventType(); +// } +// else if (event.getEventType().isPre()) { +// this.preRepository = event.getItem(); +// this.preEvent = event.getEventType(); +// } +// } +// } +// } diff --git a/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java b/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java new file mode 100644 index 0000000000..02ff0967bb --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java @@ -0,0 +1,97 @@ +package sonia.scm.update; + +import org.junit.jupiter.api.Test; +import sonia.scm.migration.UpdateStep; +import sonia.scm.store.ConfigurationEntryStoreFactory; +import sonia.scm.store.InMemoryConfigurationEntryStore; +import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; +import sonia.scm.version.Version; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static sonia.scm.version.Version.parse; + +class UpdateEngineTest { + + ConfigurationEntryStoreFactory storeFactory = new InMemoryConfigurationEntryStoreFactory(new InMemoryConfigurationEntryStore()); + + List processedUpdates = new ArrayList<>(); + + @Test + void shouldProcessStepsInCorrectOrder() { + LinkedHashSet updateSteps = new LinkedHashSet<>(); + + updateSteps.add(new FixedVersionUpdateStep("test", "1.1.1")); + updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0")); + updateSteps.add(new FixedVersionUpdateStep("test", "1.1.0")); + + UpdateEngine updateEngine = new UpdateEngine(updateSteps, storeFactory); + updateEngine.update(); + + assertThat(processedUpdates) + .containsExactly("1.1.0", "1.1.1", "1.2.0"); + } + + @Test + void shouldRunStepsOnlyOnce() { + LinkedHashSet updateSteps = new LinkedHashSet<>(); + + updateSteps.add(new FixedVersionUpdateStep("test", "1.1.1")); + + UpdateEngine updateEngine = new UpdateEngine(updateSteps, storeFactory); + updateEngine.update(); + + processedUpdates.clear(); + + updateEngine.update(); + + assertThat(processedUpdates).isEmpty(); + } + + @Test + void shouldRunStepsForDifferentTypesIndependently() { + LinkedHashSet updateSteps = new LinkedHashSet<>(); + + updateSteps.add(new FixedVersionUpdateStep("test", "1.1.1")); + + UpdateEngine updateEngine = new UpdateEngine(updateSteps, storeFactory); + updateEngine.update(); + + processedUpdates.clear(); + + updateSteps.add(new FixedVersionUpdateStep("other", "1.1.1")); + + updateEngine = new UpdateEngine(updateSteps, storeFactory); + updateEngine.update(); + + assertThat(processedUpdates).containsExactly("1.1.1"); + } + + class FixedVersionUpdateStep implements UpdateStep { + private final String type; + private final String version; + + FixedVersionUpdateStep(String type, String version) { + this.type = type; + this.version = version; + } + + @Override + public Version getTargetVersion() { + return parse(version); + } + + @Override + public String getAffectedDataType() { + return type; + } + + @Override + public void doUpdate() { + processedUpdates.add(version); + } + } +}