Merged in feature/migration (pull request #249)

Feature migration
This commit is contained in:
Sebastian Sdorra
2019-05-23 15:04:37 +00:00
32 changed files with 1669 additions and 1148 deletions

View File

@@ -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);
}
}

View File

@@ -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.
* <p>The data type can be an arbitrary string, but it is considered a best practice to use a qualified name, for
* example
* <ul>
* <li><code>com.example.myPlugin.configuration</code></li> for data in plugins, or
* <li><code>com.cloudogu.scm.repository</code></li> for core data structures.
* </ul>
* </p>
* <p>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 <i>A</i> has to run <b>before</b> another step for data type
* <i>B</i>, the version number of the second step has to be greater in regards to {@link Version#compareTo(Version)}.
* </p>
* <p>The algorithm looks something like this:<br>
* Whenever the SCM-Manager starts,
* <ul>
* <li>it creates a so called <i>bootstrap guice context</i>, that contains
* <ul>
* <li>a {@link sonia.scm.security.KeyGenerator},</li>
* <li>the {@link sonia.scm.repository.RepositoryLocationResolver},</li>
* <li>the {@link sonia.scm.io.FileSystem},</li>
* <li>the {@link sonia.scm.security.CipherHandler},</li>
* <li>a {@link sonia.scm.store.ConfigurationStoreFactory},</li>
* <li>a {@link sonia.scm.store.ConfigurationEntryStoreFactory},</li>
* <li>a {@link sonia.scm.store.DataStoreFactory},</li>
* <li>a {@link sonia.scm.store.BlobStoreFactory}, and</li>
* <li>the {@link sonia.scm.plugin.PluginLoader}.</li>
* </ul>
* Mind, that there are no DAOs, Managers or the like available at this time!
* </li>
* <li>It then checks whether there are instances of this interface that have not run before, that is either
* <ul>
* <li>their version number given by {@link #getTargetVersion()} is bigger than the last recorded target version of an
* executed update step for the data type given by {@link #getAffectedDataType()}, or
* </li>
* <li>there is no version number known for the given data type.
* </li>
* </ul>
* These are the <i>relevant</i> update steps.
* </li>
* <li>These relevant update steps are then sorted ascending by their target version given by
* {@link #getTargetVersion()}.
* </li>
* <li>Finally, these sorted steps are executed one after another calling {@link #doUpdate()} of each step, updating the
* version for the data type accordingly.
* </li>
* <li>If all works well, SCM-Manager then creates the runtime guice context by loading all further modules.</li>
* <li>If any of the update steps fails, the whole process is interrupted and SCM-Manager will not start up and will
* not record the version number of this update step.
* </li>
* </ul>
* </p>
*/
@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
* <code>com.example.myPlugin.configuration</code>.
*/
String getAffectedDataType();
}

View File

@@ -46,6 +46,7 @@ import sonia.scm.store.ConfigurationStoreFactory;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.nio.file.Path;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -172,6 +173,6 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
} }
private File resolveNativeDirectory(String repositoryId) { private File resolveNativeDirectory(String repositoryId) {
return repositoryLocationResolver.getPath(repositoryId).resolve(REPOSITORIES_NATIVE_DIRECTORY).toFile(); return repositoryLocationResolver.create(Path.class).getLocation(repositoryId).resolve(REPOSITORIES_NATIVE_DIRECTORY).toFile();
} }
} }

View File

@@ -0,0 +1,15 @@
package sonia.scm.repository;
public abstract class BasicRepositoryLocationResolver<T> extends RepositoryLocationResolver {
private final Class<T> type;
protected BasicRepositoryLocationResolver(Class<T> type) {
this.type = type;
}
@Override
public boolean supportsLocationType(Class<?> type) {
return type.isAssignableFrom(this.type);
}
}

View File

@@ -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) ;
}

View File

@@ -1,51 +1,20 @@
package sonia.scm.repository; package sonia.scm.repository;
import sonia.scm.SCMContextProvider; public abstract class RepositoryLocationResolver {
import javax.inject.Inject; public abstract boolean supportsLocationType(Class<?> type);
import java.nio.file.Path;
/** protected abstract <T> RepositoryLocationResolverInstance<T> create(Class<T> type);
* A Location Resolver for File based Repository Storage.
* <p>
* <b>WARNING:</b> The Locations provided with this class may not be used from the plugins to store any plugin specific files.
* <p>
* Please use the {@link sonia.scm.store.DataStoreFactory } and the {@link sonia.scm.store.DataStore} classes to store data<br>
* Please use the {@link sonia.scm.store.BlobStoreFactory } and the {@link sonia.scm.store.BlobStore} classes to store binary files<br>
* 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 {
private final SCMContextProvider contextProvider; public final <T> RepositoryLocationResolverInstance<T> forClass(Class<T> type) {
private final RepositoryDAO repositoryDAO; if (!supportsLocationType(type)) {
private final InitialRepositoryLocationResolver initialRepositoryLocationResolver; throw new IllegalStateException("no support for location of class " + type);
}
@Inject return create(type);
public RepositoryLocationResolver(SCMContextProvider contextProvider, RepositoryDAO repositoryDAO, InitialRepositoryLocationResolver initialRepositoryLocationResolver) {
this.contextProvider = contextProvider;
this.repositoryDAO = repositoryDAO;
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
} }
/** @FunctionalInterface
* Returns the path to the repository. public interface RepositoryLocationResolverInstance<T> {
* T getLocation(String repositoryId);
* @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);
} }
} }

View File

@@ -121,8 +121,11 @@ public class RepositoryRole implements ModelObject, PermissionObject {
* @return the hash code value for the {@link RepositoryRole} * @return the hash code value for the {@link RepositoryRole}
*/ */
@Override @Override
public int hashCode() { public int hashCode()
return Objects.hashCode(name, verbs); {
// 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 @Override

View File

@@ -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<Path>) 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);
}
}

View File

@@ -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.
* <p>
* <b>WARNING:</b> The Locations provided with this class may not be used from the plugins to store any plugin specific files.
* <p>
* Please use the {@link sonia.scm.store.DataStoreFactory } and the {@link sonia.scm.store.DataStore} classes to store data<br>
* Please use the {@link sonia.scm.store.BlobStoreFactory } and the {@link sonia.scm.store.BlobStore} classes to store binary files<br>
* 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<Path> {
private static final String STORE_NAME = "repositories";
private final SCMContextProvider contextProvider;
private final InitialRepositoryLocationResolver initialRepositoryLocationResolver;
private final PathDatabase pathDatabase;
private final Map<String, Path> 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 <T> RepositoryLocationResolverInstance<T> create(Class<T> 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<String, Path> 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));
}
}

View File

@@ -33,22 +33,18 @@ package sonia.scm.repository.xml;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import sonia.scm.SCMContextProvider;
import sonia.scm.io.FileSystem; import sonia.scm.io.FileSystem;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.PathBasedRepositoryDAO;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.store.StoreConstants; import sonia.scm.store.StoreConstants;
import javax.inject.Inject; import javax.inject.Inject;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Clock;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -57,82 +53,38 @@ import java.util.concurrent.ConcurrentHashMap;
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@Singleton @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 MetadataStore metadataStore = new MetadataStore();
private final SCMContextProvider context; private final PathBasedRepositoryLocationResolver repositoryLocationResolver;
private final InitialRepositoryLocationResolver locationResolver;
private final FileSystem fileSystem; private final FileSystem fileSystem;
private final Map<String, Path> pathById;
private final Map<String, Repository> byId; private final Map<String, Repository> byId;
private final Map<NamespaceAndName, Repository> byNamespaceAndName; private final Map<NamespaceAndName, Repository> byNamespaceAndName;
private final Clock clock;
private Long creationTime;
private Long lastModified;
@Inject @Inject
public XmlRepositoryDAO(SCMContextProvider context, InitialRepositoryLocationResolver locationResolver, FileSystem fileSystem) { public XmlRepositoryDAO(PathBasedRepositoryLocationResolver repositoryLocationResolver, FileSystem fileSystem) {
this(context, locationResolver, fileSystem, Clock.systemUTC()); this.repositoryLocationResolver = repositoryLocationResolver;
}
XmlRepositoryDAO(SCMContextProvider context, InitialRepositoryLocationResolver locationResolver, FileSystem fileSystem, Clock clock) {
this.context = context;
this.locationResolver = locationResolver;
this.fileSystem = fileSystem; this.fileSystem = fileSystem;
this.clock = clock;
this.creationTime = clock.millis();
this.pathById = new ConcurrentHashMap<>();
this.byId = new ConcurrentHashMap<>(); this.byId = new ConcurrentHashMap<>();
this.byNamespaceAndName = new ConcurrentHashMap<>(); this.byNamespaceAndName = new ConcurrentHashMap<>();
pathDatabase = new PathDatabase(resolveStorePath()); init();
read();
} }
private void read() { private void init() {
Path storePath = resolveStorePath(); repositoryLocationResolver.forAllPaths((repositoryId, repositoryPath) -> {
Path metadataPath = resolveDataPath(repositoryPath);
// 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;
}
private void onLoadRepository(String id, Path repositoryPath) {
Path metadataPath = resolveMetadataPath(context.resolve(repositoryPath));
Repository repository = metadataStore.read(metadataPath); Repository repository = metadataStore.read(metadataPath);
byId.put(id, repository);
byNamespaceAndName.put(repository.getNamespaceAndName(), repository); byNamespaceAndName.put(repository.getNamespaceAndName(), repository);
pathById.put(id, repositoryPath); byId.put(repositoryId, repository);
});
} }
@VisibleForTesting private Path resolveDataPath(Path repositoryPath) {
Path resolveStorePath() {
return context.getBaseDirectory()
.toPath()
.resolve(StoreConstants.CONFIG_DIRECTORY_NAME)
.resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION));
}
@VisibleForTesting
Path resolveMetadataPath(Path repositoryPath) {
return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION)); return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION));
} }
@@ -141,47 +93,27 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO {
return "xml"; return "xml";
} }
@Override
public Long getCreationTime() {
return creationTime;
}
@Override
public Long getLastModified() {
return lastModified;
}
@Override @Override
public void add(Repository repository) { public void add(Repository repository) {
Repository clone = repository.clone(); Repository clone = repository.clone();
Path repositoryPath = locationResolver.getPath(repository.getId()); synchronized (this) {
Path resolvedPath = context.resolve(repositoryPath); Path repositoryPath = repositoryLocationResolver.create(repository.getId());
try { try {
fileSystem.create(resolvedPath.toFile()); Path metadataPath = resolveDataPath(repositoryPath);
Path metadataPath = resolveMetadataPath(resolvedPath);
metadataStore.write(metadataPath, repository); metadataStore.write(metadataPath, repository);
} catch (Exception e) {
synchronized (this) { repositoryLocationResolver.remove(repository.getId());
pathById.put(repository.getId(), repositoryPath); throw new InternalRepositoryException(repository, "failed to create filesystem", e);
}
byId.put(repository.getId(), clone); byId.put(repository.getId(), clone);
byNamespaceAndName.put(repository.getNamespaceAndName(), clone); byNamespaceAndName.put(repository.getNamespaceAndName(), clone);
writePathDatabase();
} }
} catch (IOException e) {
throw new InternalRepositoryException(repository, "failed to create filesystem", e);
}
} }
private void writePathDatabase() {
lastModified = clock.millis();
pathDatabase.write(creationTime, lastModified, pathById);
}
@Override @Override
public boolean contains(Repository repository) { public boolean contains(Repository repository) {
@@ -224,12 +156,13 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO {
byNamespaceAndName.remove(prev.getNamespaceAndName()); byNamespaceAndName.remove(prev.getNamespaceAndName());
} }
byNamespaceAndName.put(clone.getNamespaceAndName(), clone); byNamespaceAndName.put(clone.getNamespaceAndName(), clone);
writePathDatabase();
} }
Path repositoryPath = context.resolve(getPath(repository.getId())); Path repositoryPath = repositoryLocationResolver
Path metadataPath = resolveMetadataPath(repositoryPath); .create(Path.class)
.getLocation(repository.getId());
Path metadataPath = resolveDataPath(repositoryPath);
repositoryLocationResolver.updateModificationDate();
metadataStore.write(metadataPath, clone); metadataStore.write(metadataPath, clone);
} }
@@ -241,14 +174,9 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO {
if (prev != null) { if (prev != null) {
byNamespaceAndName.remove(prev.getNamespaceAndName()); byNamespaceAndName.remove(prev.getNamespaceAndName());
} }
path = repositoryLocationResolver.remove(repository.getId());
path = pathById.remove(repository.getId());
writePathDatabase();
} }
path = context.resolve(path);
try { try {
fileSystem.destroy(path.toFile()); fileSystem.destroy(path.toFile());
} catch (IOException e) { } catch (IOException e) {
@@ -257,7 +185,12 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO {
} }
@Override @Override
public Path getPath(String repositoryId) { public Long getCreationTime() {
return pathById.get(repositoryId); return repositoryLocationResolver.getCreationTime();
}
@Override
public Long getLastModified() {
return repositoryLocationResolver.getLastModified();
} }
} }

View File

@@ -40,6 +40,7 @@ import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
import java.io.File; import java.io.File;
import java.nio.file.Path;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -92,7 +93,7 @@ public abstract class FileBasedStoreFactory {
* @return the store directory of a specific repository * @return the store directory of a specific repository
*/ */
private File getStoreDirectory(Store store, Repository 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());
} }
/** /**

View File

@@ -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<String, Path> 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);
}
}
}

View File

@@ -2,78 +2,81 @@ package sonia.scm.repository.xml;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory; import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; import org.mockito.quality.Strictness;
import sonia.scm.SCMContextProvider;
import sonia.scm.io.DefaultFileSystem; import sonia.scm.io.DefaultFileSystem;
import sonia.scm.io.FileSystem; import sonia.scm.io.FileSystem;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryTestData;
import java.io.IOException; import java.io.IOException;
import java.net.URL;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Clock;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.function.BiConsumer;
import java.util.concurrent.atomic.AtomicLong;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.mock; 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; import static org.mockito.Mockito.when;
@ExtendWith({MockitoExtension.class, TempDirectory.class}) @ExtendWith({MockitoExtension.class, TempDirectory.class})
@MockitoSettings(strictness = Strictness.LENIENT) @MockitoSettings(strictness = Strictness.LENIENT)
class XmlRepositoryDAOTest { class XmlRepositoryDAOTest {
@Mock private final Repository REPOSITORY = createRepository("42");
private SCMContextProvider context;
@Mock @Mock
private InitialRepositoryLocationResolver locationResolver; private PathBasedRepositoryLocationResolver locationResolver;
@Captor
private ArgumentCaptor<BiConsumer<String, Path>> forAllCaptor;
private FileSystem fileSystem = new DefaultFileSystem(); private FileSystem fileSystem = new DefaultFileSystem();
private XmlRepositoryDAO dao; private XmlRepositoryDAO dao;
private Path baseDirectory;
private AtomicLong atomicClock;
@BeforeEach @BeforeEach
void createDAO(@TempDirectory.TempDir Path baseDirectory) { void createDAO(@TempDirectory.TempDir Path basePath) {
this.baseDirectory = baseDirectory; when(locationResolver.create(Path.class)).thenReturn(locationResolver::create);
this.atomicClock = new AtomicLong(); when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation));
when(locationResolver.remove(anyString())).thenAnswer(invocation -> basePath.resolve(invocation.getArgument(0).toString()));
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();
} }
private XmlRepositoryDAO createDAO() { private Path createMockedRepoPath(@TempDirectory.TempDir Path basePath, InvocationOnMock invocation) {
Clock clock = mock(Clock.class); Path resolvedPath = basePath.resolve(invocation.getArgument(0).toString());
when(clock.millis()).then(ic -> atomicClock.incrementAndGet()); 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 @Test
@@ -82,39 +85,63 @@ class XmlRepositoryDAOTest {
} }
@Test @Test
void shouldReturnCreationTimeAfterCreation() { void shouldReturnCreationTimeOfLocationResolver() {
long now = atomicClock.get(); long now = 42L;
when(locationResolver.getCreationTime()).thenReturn(now);
assertThat(dao.getCreationTime()).isEqualTo(now); assertThat(dao.getCreationTime()).isEqualTo(now);
} }
@Test @Test
void shouldNotReturnLastModifiedAfterCreation() { void shouldReturnLasModifiedOfLocationResolver() {
assertThat(dao.getLastModified()).isNull(); long now = 42L;
when(locationResolver.getLastModified()).thenReturn(now);
assertThat(dao.getLastModified()).isEqualTo(now);
} }
@Test @Test
void shouldReturnTrueForEachContainsMethod() { void shouldReturnTrueForEachContainsMethod() {
Repository heartOfGold = createHeartOfGold(); dao.add(REPOSITORY);
dao.add(heartOfGold);
assertThat(dao.contains(heartOfGold)).isTrue(); assertThat(dao.contains(REPOSITORY)).isTrue();
assertThat(dao.contains(heartOfGold.getId())).isTrue(); assertThat(dao.contains(REPOSITORY.getId())).isTrue();
assertThat(dao.contains(heartOfGold.getNamespaceAndName())).isTrue(); assertThat(dao.contains(REPOSITORY.getNamespaceAndName())).isTrue();
} }
private Repository createHeartOfGold() { @Test
Repository heartOfGold = RepositoryTestData.createHeartOfGold(); void shouldPersistRepository() {
heartOfGold.setId("42"); dao.add(REPOSITORY);
return heartOfGold;
String content = getXmlFileContent(REPOSITORY.getId());
assertThat(content).contains("<id>42</id>");
}
@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 @Test
void shouldReturnFalseForEachContainsMethod() { void shouldReturnFalseForEachContainsMethod() {
Repository heartOfGold = createHeartOfGold(); assertThat(dao.contains(REPOSITORY)).isFalse();
assertThat(dao.contains(REPOSITORY.getId())).isFalse();
assertThat(dao.contains(heartOfGold)).isFalse(); assertThat(dao.contains(REPOSITORY.getNamespaceAndName())).isFalse();
assertThat(dao.contains(heartOfGold.getId())).isFalse();
assertThat(dao.contains(heartOfGold.getNamespaceAndName())).isFalse();
} }
@Test @Test
@@ -125,48 +152,39 @@ class XmlRepositoryDAOTest {
@Test @Test
void shouldReturnRepository() { void shouldReturnRepository() {
Repository heartOfGold = createHeartOfGold(); dao.add(REPOSITORY);
dao.add(heartOfGold);
assertThat(dao.get("42")).isEqualTo(heartOfGold); assertThat(dao.get("42")).isEqualTo(REPOSITORY);
assertThat(dao.get(new NamespaceAndName("hitchhiker","HeartOfGold"))).isEqualTo(heartOfGold); assertThat(dao.get(new NamespaceAndName("space", "42"))).isEqualTo(REPOSITORY);
} }
@Test @Test
void shouldNotReturnTheSameInstance() { void shouldNotReturnTheSameInstance() {
Repository heartOfGold = createHeartOfGold(); dao.add(REPOSITORY);
dao.add(heartOfGold);
Repository repository = dao.get("42"); Repository repository = dao.get("42");
assertThat(repository).isNotSameAs(heartOfGold); assertThat(repository).isNotSameAs(REPOSITORY);
} }
@Test @Test
void shouldReturnAllRepositories() { void shouldReturnAllRepositories() {
Repository heartOfGold = createHeartOfGold(); dao.add(REPOSITORY);
dao.add(heartOfGold);
Repository puzzle = createPuzzle(); Repository secondRepository = createRepository("23");
dao.add(puzzle); dao.add(secondRepository);
Collection<Repository> repositories = dao.getAll(); Collection<Repository> repositories = dao.getAll();
assertThat(repositories).containsExactlyInAnyOrder(heartOfGold, puzzle); assertThat(repositories)
} .containsExactlyInAnyOrder(REPOSITORY, secondRepository);
private Repository createPuzzle() {
Repository puzzle = RepositoryTestData.create42Puzzle();
puzzle.setId("42+1");
return puzzle;
} }
@Test @Test
void shouldModifyRepository() { void shouldModifyRepositoryTwice() {
Repository heartOfGold = createHeartOfGold(); REPOSITORY.setDescription("HeartOfGold");
heartOfGold.setDescription("HeartOfGold"); dao.add(REPOSITORY);
dao.add(heartOfGold);
assertThat(dao.get("42").getDescription()).isEqualTo("HeartOfGold"); assertThat(dao.get("42").getDescription()).isEqualTo("HeartOfGold");
heartOfGold = createHeartOfGold(); Repository heartOfGold = createRepository("42");
heartOfGold.setDescription("Heart of Gold"); heartOfGold.setDescription("Heart of Gold");
dao.modify(heartOfGold); dao.modify(heartOfGold);
@@ -175,50 +193,26 @@ class XmlRepositoryDAOTest {
@Test @Test
void shouldRemoveRepository() { void shouldRemoveRepository() {
Repository heartOfGold = createHeartOfGold(); dao.add(REPOSITORY);
dao.add(heartOfGold);
assertThat(dao.contains("42")).isTrue(); assertThat(dao.contains("42")).isTrue();
dao.delete(heartOfGold); dao.delete(REPOSITORY);
assertThat(dao.contains("42")).isFalse(); assertThat(dao.contains("42")).isFalse();
assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse(); assertThat(dao.contains(REPOSITORY.getNamespaceAndName())).isFalse();
}
@Test Path storePath = metadataFile(REPOSITORY.getId());
void shouldUpdateLastModifiedAfterEachWriteOperation() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Long firstLastModified = dao.getLastModified(); assertThat(storePath).doesNotExist();
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 @Test
void shouldRenameTheRepository() { void shouldRenameTheRepository() {
Repository heartOfGold = createHeartOfGold(); dao.add(REPOSITORY);
dao.add(heartOfGold);
heartOfGold.setNamespace("hg2tg"); REPOSITORY.setNamespace("hg2tg");
heartOfGold.setName("hog"); REPOSITORY.setName("hog");
dao.modify(heartOfGold); dao.modify(REPOSITORY);
Repository repository = dao.get("42"); Repository repository = dao.get("42");
assertThat(repository.getNamespace()).isEqualTo("hg2tg"); assertThat(repository.getNamespace()).isEqualTo("hg2tg");
@@ -226,152 +220,91 @@ class XmlRepositoryDAOTest {
assertThat(dao.contains(new NamespaceAndName("hg2tg", "hog"))).isTrue(); assertThat(dao.contains(new NamespaceAndName("hg2tg", "hog"))).isTrue();
assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse(); assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse();
String content = getXmlFileContent(REPOSITORY.getId());
assertThat(content).contains("<name>hog</name>");
} }
@Test @Test
void shouldDeleteRepositoryEvenWithChangedNamespace() { void shouldDeleteRepositoryEvenWithChangedNamespace() {
Repository heartOfGold = createHeartOfGold(); dao.add(REPOSITORY);
dao.add(heartOfGold);
heartOfGold.setNamespace("hg2tg"); REPOSITORY.setNamespace("hg2tg");
heartOfGold.setName("hog"); REPOSITORY.setName("hog");
dao.delete(heartOfGold); dao.delete(REPOSITORY);
assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse(); assertThat(dao.contains(new NamespaceAndName("space", "42"))).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 @Test
void shouldRemoveRepositoryDirectoryAfterDeletion() { void shouldRemoveRepositoryDirectoryAfterDeletion() {
Repository heartOfGold = createHeartOfGold(); dao.add(REPOSITORY);
dao.add(heartOfGold);
Path path = getAbsolutePathFromDao(heartOfGold.getId()); Path path = locationResolver.create(REPOSITORY.getId());
assertThat(path).isDirectory(); assertThat(path).isDirectory();
dao.delete(heartOfGold); dao.delete(REPOSITORY);
assertThat(path).doesNotExist(); assertThat(path).doesNotExist();
} }
private Path getAbsolutePathFromDao(String id) {
return context.resolve(dao.getPath(id));
}
@Test @Test
void shouldCreateRepositoryPathDatabase() throws IOException { void shouldPersistPermissions() {
Repository heartOfGold = createHeartOfGold(); REPOSITORY.setPermissions(asList(new RepositoryPermission("trillian", asList("read", "write"), false), new RepositoryPermission("vogons", singletonList("delete"), true)));
dao.add(heartOfGold); dao.add(REPOSITORY);
Path storePath = dao.resolveStorePath(); String content = getXmlFileContent(REPOSITORY.getId());
assertThat(storePath).isRegularFile();
String content = content(storePath);
assertThat(content).contains(heartOfGold.getId());
assertThat(content).contains(dao.getPath(heartOfGold.getId()).toString());
}
private String content(Path storePath) throws IOException {
return new String(Files.readAllBytes(storePath), Charsets.UTF_8);
}
@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());
}
@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", "<verb>read</verb>", "<verb>write</verb>"); assertThat(content).containsSubsequence("trillian", "<verb>read</verb>", "<verb>write</verb>");
assertThat(content).containsSubsequence("vogons", "<verb>delete</verb>"); assertThat(content).containsSubsequence("vogons", "<verb>delete</verb>");
} }
@Test @Test
void shouldReadPathDatabaseAndMetadataOfRepositories() { void shouldUpdateRepositoryPathDatabse() {
Repository heartOfGold = createHeartOfGold(); dao.add(REPOSITORY);
dao.add(heartOfGold);
// reload data verify(locationResolver, never()).updateModificationDate();
dao = createDAO();
heartOfGold = dao.get("42"); dao.modify(REPOSITORY);
assertThat(heartOfGold.getName()).isEqualTo("HeartOfGold");
Path path = getAbsolutePathFromDao(heartOfGold.getId()); verify(locationResolver).updateModificationDate();
assertThat(path).isDirectory(); }
} }
@Test @Test
void shouldReadCreationTimeAndLastModifedDateFromDatabase() { void shouldReadExistingRepositoriesFromPathDatabase(@TempDirectory.TempDir Path basePath) throws IOException {
Repository heartOfGold = createHeartOfGold(); doNothing().when(locationResolver).forAllPaths(forAllCaptor.capture());
dao.add(heartOfGold); XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
Long creationTime = dao.getCreationTime(); Path repositoryPath = basePath.resolve("existing");
Long lastModified = dao.getLastModified(); Files.createDirectories(repositoryPath);
URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml");
Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml"));
// reload data forAllCaptor.getValue().accept("existing", repositoryPath);
dao = createDAO();
assertThat(dao.getCreationTime()).isEqualTo(creationTime); assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
assertThat(dao.getLastModified()).isEqualTo(lastModified); }
private String getXmlFileContent(String id) {
Path storePath = metadataFile(id);
assertThat(storePath).isRegularFile();
return content(storePath);
}
private Path metadataFile(String id) {
return locationResolver.create(id).resolve("metadata.xml");
}
private String content(Path storePath) {
try {
return new String(Files.readAllBytes(storePath), Charsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static Repository createRepository(String id) {
return new Repository(id, "xml", "space", id);
} }
} }

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<repositories>
<properties/>
<id>existing</id>
<namespace>space</namespace>
<name>existing</name>
<public>false</public>
<archived>false</archived>
<type>xml</type>
</repositories>

View File

@@ -26,7 +26,7 @@ public class GitRepositoryContextResolver implements RepositoryContextResolver {
public RepositoryContext resolve(String[] args) { public RepositoryContext resolve(String[] args) {
NamespaceAndName namespaceAndName = extractNamespaceAndName(args); NamespaceAndName namespaceAndName = extractNamespaceAndName(args);
Repository repository = repositoryManager.get(namespaceAndName); 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); return new RepositoryContext(repository, path);
} }

View File

@@ -2,6 +2,7 @@ package sonia.scm.protocolcommand.git;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
@@ -16,6 +17,7 @@ import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@@ -25,7 +27,7 @@ class GitRepositoryContextResolverTest {
@Mock @Mock
RepositoryManager repositoryManager; RepositoryManager repositoryManager;
@Mock @Mock(answer = Answers.RETURNS_DEEP_STUBS)
RepositoryLocationResolver locationResolver; RepositoryLocationResolver locationResolver;
@InjectMocks @InjectMocks
@@ -35,7 +37,7 @@ class GitRepositoryContextResolverTest {
void shouldResolveCorrectRepository() throws IOException { void shouldResolveCorrectRepository() throws IOException {
when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(REPOSITORY); when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(REPOSITORY);
Path repositoryPath = File.createTempFile("test", "scm").toPath(); 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"}); RepositoryContext context = resolver.resolve(new String[] {"git", "repo/space/X/something/else"});

View File

@@ -37,6 +37,7 @@ package sonia.scm.repository;
import org.junit.Assume; import org.junit.Assume;
import sonia.scm.SCMContext; import sonia.scm.SCMContext;
import sonia.scm.TempDirRepositoryLocationResolver;
import sonia.scm.store.InMemoryConfigurationStoreFactory; import sonia.scm.store.InMemoryConfigurationStoreFactory;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -101,13 +102,12 @@ public final class HgTestUtil
context.setBaseDirectory(directory); 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 = HgRepositoryHandler handler =
new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null, null); new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null, null);
Path repoDir = directory.toPath(); Path repoDir = directory.toPath();
when(repoDao.getPath(any())).thenReturn(repoDir);
handler.init(context); handler.init(context);
return handler; return handler;

View File

@@ -91,7 +91,7 @@ public class AbstractTestBase
contextProvider = MockUtil.getSCMContextProvider(tempDirectory); contextProvider = MockUtil.getSCMContextProvider(tempDirectory);
fileSystem = new DefaultFileSystem(); fileSystem = new DefaultFileSystem();
InitialRepositoryLocationResolver initialRepoLocationResolver = new InitialRepositoryLocationResolver(); InitialRepositoryLocationResolver initialRepoLocationResolver = new InitialRepositoryLocationResolver();
repositoryLocationResolver = new RepositoryLocationResolver(contextProvider, repositoryDAO, initialRepoLocationResolver); repositoryLocationResolver = new TempDirRepositoryLocationResolver(tempDirectory);
postSetUp(); postSetUp();
} }
@@ -254,4 +254,5 @@ public class AbstractTestBase
subjectThreadState = createThreadState(subject); subjectThreadState = createThreadState(subject);
subjectThreadState.bind(); subjectThreadState.bind();
} }
} }

View File

@@ -74,7 +74,7 @@ public abstract class ManagerTestBase<T extends ModelObject>
contextProvider = MockUtil.getSCMContextProvider(temp); contextProvider = MockUtil.getSCMContextProvider(temp);
InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(); InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver();
RepositoryDAO repoDao = mock(RepositoryDAO.class); RepositoryDAO repoDao = mock(RepositoryDAO.class);
locationResolver = new RepositoryLocationResolver(contextProvider, repoDao ,initialRepositoryLocationResolver); locationResolver = new TempDirRepositoryLocationResolver(temp);
manager = createManager(); manager = createManager();
manager.init(contextProvider); manager.init(contextProvider);
} }

View File

@@ -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 <T> RepositoryLocationResolverInstance<T> create(Class<T> type) {
return repositoryId -> (T) tempDirectory.toPath();
}
}

View File

@@ -44,6 +44,7 @@ import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -56,7 +57,7 @@ import static org.mockito.Mockito.when;
public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase { public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase {
protected PathBasedRepositoryDAO repoDao = mock(PathBasedRepositoryDAO.class); protected RepositoryDAO repoDao = mock(RepositoryDAO.class);
protected Path repoPath; protected Path repoPath;
protected Repository repository; protected Repository repository;
@@ -78,7 +79,11 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase {
locationResolver = mock(RepositoryLocationResolver.class); 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); String id = ic.getArgument(0);
return baseDirectory.toPath().resolve(id); return baseDirectory.toPath().resolve(id);
}); });
@@ -107,7 +112,7 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase {
repository = RepositoryTestData.createHeartOfGold(); repository = RepositoryTestData.createHeartOfGold();
File repoDirectory = new File(baseDirectory, repository.getId()); File repoDirectory = new File(baseDirectory, repository.getId());
repoPath = repoDirectory.toPath(); 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); return new File(repoDirectory, AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
} }

View File

@@ -184,6 +184,12 @@
<version>${guice.version}</version> <version>${guice.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-assistedinject</artifactId>
<version>${guice.version}</version>
</dependency>
<!-- event bus --> <!-- event bus -->
<dependency> <dependency>

View File

@@ -37,6 +37,7 @@ import com.google.common.base.Throwables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.inject.Injector; import com.google.inject.Injector;
import com.google.inject.Module; import com.google.inject.Module;
import com.google.inject.assistedinject.Assisted;
import org.apache.shiro.guice.web.ShiroWebModule; import org.apache.shiro.guice.web.ShiroWebModule;
import org.jboss.resteasy.plugins.guice.GuiceResteasyBootstrapServletContextListener; import org.jboss.resteasy.plugins.guice.GuiceResteasyBootstrapServletContextListener;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -55,6 +56,7 @@ import sonia.scm.upgrade.UpgradeManager;
import sonia.scm.user.UserManager; import sonia.scm.user.UserManager;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
import javax.inject.Inject;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextEvent;
import java.util.Collections; import java.util.Collections;
@@ -77,9 +79,12 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
private final Set<PluginWrapper> plugins; private final Set<PluginWrapper> plugins;
private Injector injector; private Injector injector;
//~--- constructors --------------------------------------------------------- public interface Factory {
ScmContextListener create(ClassLoader parent, Set<PluginWrapper> plugins);
}
public ScmContextListener(ClassLoader parent, Set<PluginWrapper> plugins) @Inject
public ScmContextListener(@Assisted ClassLoader parent, @Assisted Set<PluginWrapper> plugins)
{ {
this.parent = parent; this.parent = parent;
this.plugins = plugins; this.plugins = plugins;
@@ -127,9 +132,6 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
List<Module> moduleList = Lists.newArrayList(); List<Module> moduleList = Lists.newArrayList();
moduleList.add(new ResteasyModule()); moduleList.add(new ResteasyModule());
moduleList.add(new ScmInitializerModule());
moduleList.add(new ScmEventBusModule());
moduleList.add(new EagerSingletonModule());
moduleList.add(ShiroWebModule.guiceFilterModule()); moduleList.add(ShiroWebModule.guiceFilterModule());
moduleList.add(new WebElementModule(pluginLoader)); moduleList.add(new WebElementModule(pluginLoader));
moduleList.add(new ScmServletModule(context, pluginLoader, overrides)); moduleList.add(new ScmServletModule(context, pluginLoader, overrides));

View File

@@ -212,12 +212,7 @@ public class ScmServletModule extends ServletModule
{ {
install(ThrowingProviderBinder.forModule(this)); install(ThrowingProviderBinder.forModule(this));
SCMContextProvider context = SCMContext.getContext();
bind(SCMContextProvider.class).toInstance(context);
ScmConfiguration config = getScmConfiguration(); ScmConfiguration config = getScmConfiguration();
CipherUtil cu = CipherUtil.getInstance();
bind(NamespaceStrategy.class).toProvider(NamespaceStrategyProvider.class); bind(NamespaceStrategy.class).toProvider(NamespaceStrategyProvider.class);
@@ -234,22 +229,12 @@ public class ScmServletModule extends ServletModule
bind(ScmEventBus.class).toInstance(ScmEventBus.getInstance()); bind(ScmEventBus.class).toInstance(ScmEventBus.getInstance());
// bind core // 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(ScmConfiguration.class).toInstance(config);
bind(PluginLoader.class).toInstance(pluginLoader);
bind(PluginManager.class, DefaultPluginManager.class); bind(PluginManager.class, DefaultPluginManager.class);
// bind scheduler // bind scheduler
bind(Scheduler.class).to(QuartzScheduler.class); 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 health check stuff
bind(HealthCheckContextListener.class); bind(HealthCheckContextListener.class);
@@ -327,7 +312,6 @@ public class ScmServletModule extends ServletModule
bind(ObjectMapper.class).toProvider(ObjectMapperProvider.class); bind(ObjectMapper.class).toProvider(ObjectMapperProvider.class);
// bind events // bind events
// bind(LastModifiedUpdateListener.class);
bind(AccessTokenCookieIssuer.class).to(DefaultAccessTokenCookieIssuer.class); bind(AccessTokenCookieIssuer.class).to(DefaultAccessTokenCookieIssuer.class);
bind(PushStateDispatcher.class).toProvider(PushStateDispatcherProvider.class); bind(PushStateDispatcher.class).toProvider(PushStateDispatcherProvider.class);

View File

@@ -1,9 +1,9 @@
/** /**
* Copyright (c) 2010, Sebastian Sdorra All rights reserved. * Copyright (c) 2010, Sebastian Sdorra All rights reserved.
* * <p>
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
* * <p>
* 1. Redistributions of source code must retain the above copyright notice, * 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. 2. Redistributions in * this list of conditions and the following disclaimer. 2. Redistributions in
* binary form must reproduce the above copyright notice, this list of * 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; * 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 * nor the names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission. * derived from this software without specific prior written permission.
* * <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * 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, * 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 * 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. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* * <p>
* http://bitbucket.org/sdorra/scm-manager * http://bitbucket.org/sdorra/scm-manager
*
*/ */
package sonia.scm.boot; package sonia.scm.boot;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.io.Files; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.EagerSingletonModule;
import sonia.scm.SCMContext; import sonia.scm.SCMContext;
import sonia.scm.ScmContextListener; import sonia.scm.ScmContextListener;
import sonia.scm.ScmEventBusModule;
import sonia.scm.ScmInitializerModule;
import sonia.scm.Stage; import sonia.scm.Stage;
import sonia.scm.event.ScmEventBus; import sonia.scm.event.ScmEventBus;
import sonia.scm.plugin.DefaultPluginLoader;
import sonia.scm.plugin.Plugin; import sonia.scm.plugin.Plugin;
import sonia.scm.plugin.PluginException; import sonia.scm.plugin.PluginException;
import sonia.scm.plugin.PluginLoadException; import sonia.scm.plugin.PluginLoadException;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.PluginWrapper; import sonia.scm.plugin.PluginWrapper;
import sonia.scm.plugin.PluginsInternal; import sonia.scm.plugin.PluginsInternal;
import sonia.scm.plugin.SmpArchive; import sonia.scm.plugin.SmpArchive;
import sonia.scm.update.UpdateEngine;
import sonia.scm.util.ClassLoaders; import sonia.scm.util.ClassLoaders;
import sonia.scm.util.IOUtil; 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.ServletContext;
import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener; import javax.servlet.ServletContextListener;
import javax.xml.bind.DataBindingException; import javax.xml.bind.DataBindingException;
import javax.xml.bind.JAXB; import javax.xml.bind.JAXB;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement; 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 * @author Sebastian Sdorra
*/ */
public class BootstrapContextListener implements ServletContextListener public class BootstrapContextListener implements ServletContextListener {
{
/** Field description */ /** Field description */
private static final String DIRECTORY_PLUGINS = "plugins"; private static final String DIRECTORY_PLUGINS = "plugins";
@@ -109,22 +107,16 @@ public class BootstrapContextListener implements ServletContextListener
* @param sce * @param sce
*/ */
@Override @Override
public void contextDestroyed(ServletContextEvent sce) public void contextDestroyed(ServletContextEvent sce) {
{
contextListener.contextDestroyed(sce); contextListener.contextDestroyed(sce);
for (PluginWrapper plugin : contextListener.getPlugins()) for (PluginWrapper plugin : contextListener.getPlugins()) {
{
ClassLoader pcl = plugin.getClassLoader(); ClassLoader pcl = plugin.getClassLoader();
if (pcl instanceof Closeable) if (pcl instanceof Closeable) {
{ try {
try
{
((Closeable) pcl).close(); ((Closeable) pcl).close();
} } catch (IOException ex) {
catch (IOException ex)
{
logger.warn("could not close plugin classloader", ex); logger.warn("could not close plugin classloader", ex);
} }
} }
@@ -141,43 +133,68 @@ public class BootstrapContextListener implements ServletContextListener
* @param sce * @param sce
*/ */
@Override @Override
public void contextInitialized(ServletContextEvent sce) public void contextInitialized(ServletContextEvent sce) {
{
context = sce.getServletContext(); context = sce.getServletContext();
File pluginDirectory = getPluginDirectory(); 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()) { if (!isCorePluginExtractionDisabled()) {
extractCorePlugins(context, pluginDirectory); extractCorePlugins(context, pluginDirectory);
} else { } else {
logger.info("core plugin extraction is disabled"); logger.info("core plugin extraction is disabled");
} }
ClassLoader cl = ClassLoader cl = ClassLoaders.getContextClassLoader(BootstrapContextListener.class);
ClassLoaders.getContextClassLoader(BootstrapContextListener.class);
Set<PluginWrapper> plugins = PluginsInternal.collectPlugins(cl, Set<PluginWrapper> plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath());
pluginDirectory.toPath());
contextListener = new ScmContextListener(cl, plugins); PluginLoader pluginLoader = new DefaultPluginLoader(context, cl, plugins);
}
catch (IOException ex) 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); 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() { private boolean isCorePluginExtractionDisabled() {
@@ -196,8 +213,7 @@ public class BootstrapContextListener implements ServletContextListener
*/ */
private void extractCorePlugin(ServletContext context, File pluginDirectory, private void extractCorePlugin(ServletContext context, File pluginDirectory,
PluginIndexEntry entry) PluginIndexEntry entry)
throws IOException throws IOException {
{
URL url = context.getResource(PLUGIN_DIRECTORY.concat(entry.getName())); URL url = context.getResource(PLUGIN_DIRECTORY.concat(entry.getName()));
SmpArchive archive = SmpArchive.create(url); SmpArchive archive = SmpArchive.create(url);
Plugin plugin = archive.getPlugin(); Plugin plugin = archive.getPlugin();
@@ -206,30 +222,22 @@ public class BootstrapContextListener implements ServletContextListener
plugin); plugin);
File checksumFile = PluginsInternal.getChecksumFile(directory); File checksumFile = PluginsInternal.getChecksumFile(directory);
if (!directory.exists()) if (!directory.exists()) {
{
logger.warn("install plugin {}", plugin.getInformation().getId()); logger.warn("install plugin {}", plugin.getInformation().getId());
PluginsInternal.extract(archive, entry.getChecksum(), directory, PluginsInternal.extract(archive, entry.getChecksum(), directory,
checksumFile, true); checksumFile, true);
} } else if (!checksumFile.exists()) {
else if (!checksumFile.exists())
{
logger.warn("plugin directory {} exists without checksum file.", logger.warn("plugin directory {} exists without checksum file.",
directory); directory);
PluginsInternal.extract(archive, entry.getChecksum(), directory, PluginsInternal.extract(archive, entry.getChecksum(), directory,
checksumFile, true); checksumFile, true);
} } else {
else
{
String checksum = Files.toString(checksumFile, Charsets.UTF_8).trim(); 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", logger.debug("plugin {} is up to date",
plugin.getInformation().getId()); plugin.getInformation().getId());
} } else {
else
{
logger.warn("checksum mismatch of pluing {}, start update", logger.warn("checksum mismatch of pluing {}, start update",
plugin.getInformation().getId()); plugin.getInformation().getId());
PluginsInternal.extract(archive, entry.getChecksum(), directory, PluginsInternal.extract(archive, entry.getChecksum(), directory,
@@ -247,14 +255,12 @@ public class BootstrapContextListener implements ServletContextListener
* *
* @throws IOException * @throws IOException
*/ */
private void extractCorePlugins(ServletContext context, File pluginDirectory) throws IOException private void extractCorePlugins(ServletContext context, File pluginDirectory) throws IOException {
{
IOUtil.mkdirs(pluginDirectory); IOUtil.mkdirs(pluginDirectory);
PluginIndex index = readCorePluginIndex(context); PluginIndex index = readCorePluginIndex(context);
for (PluginIndexEntry entry : index) for (PluginIndexEntry entry : index) {
{
extractCorePlugin(context, pluginDirectory, entry); extractCorePlugin(context, pluginDirectory, entry);
} }
} }
@@ -267,27 +273,20 @@ public class BootstrapContextListener implements ServletContextListener
* *
* @return * @return
*/ */
private PluginIndex readCorePluginIndex(ServletContext context) private PluginIndex readCorePluginIndex(ServletContext context) {
{
PluginIndex index = null; PluginIndex index = null;
try try {
{
URL indexUrl = context.getResource(PLUGIN_COREINDEX); URL indexUrl = context.getResource(PLUGIN_COREINDEX);
if (indexUrl == null) if (indexUrl == null) {
{
throw new PluginException("no core plugin index found"); throw new PluginException("no core plugin index found");
} }
index = JAXB.unmarshal(indexUrl, PluginIndex.class); index = JAXB.unmarshal(indexUrl, PluginIndex.class);
} } catch (MalformedURLException ex) {
catch (MalformedURLException ex)
{
throw new PluginException("could not load core plugin index", 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); throw new PluginException("could not unmarshall core plugin index", ex);
} }
@@ -302,8 +301,7 @@ public class BootstrapContextListener implements ServletContextListener
* *
* @return * @return
*/ */
private File getPluginDirectory() private File getPluginDirectory() {
{
File baseDirectory = SCMContext.getContext().getBaseDirectory(); File baseDirectory = SCMContext.getContext().getBaseDirectory();
return new File(baseDirectory, DIRECTORY_PLUGINS); return new File(baseDirectory, DIRECTORY_PLUGINS);
@@ -320,8 +318,7 @@ public class BootstrapContextListener implements ServletContextListener
*/ */
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "plugin-index") @XmlRootElement(name = "plugin-index")
private static class PluginIndex implements Iterable<PluginIndexEntry> private static class PluginIndex implements Iterable<PluginIndexEntry> {
{
/** /**
* Method description * Method description
@@ -330,8 +327,7 @@ public class BootstrapContextListener implements ServletContextListener
* @return * @return
*/ */
@Override @Override
public Iterator<PluginIndexEntry> iterator() public Iterator<PluginIndexEntry> iterator() {
{
return getPlugins().iterator(); return getPlugins().iterator();
} }
@@ -343,10 +339,8 @@ public class BootstrapContextListener implements ServletContextListener
* *
* @return * @return
*/ */
public List<PluginIndexEntry> getPlugins() public List<PluginIndexEntry> getPlugins() {
{ if (plugins == null) {
if (plugins == null)
{
plugins = ImmutableList.of(); plugins = ImmutableList.of();
} }
@@ -370,8 +364,7 @@ public class BootstrapContextListener implements ServletContextListener
*/ */
@XmlRootElement(name = "plugins") @XmlRootElement(name = "plugins")
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
private static class PluginIndexEntry private static class PluginIndexEntry {
{
/** /**
* Method description * Method description
@@ -379,8 +372,7 @@ public class BootstrapContextListener implements ServletContextListener
* *
* @return * @return
*/ */
public String getChecksum() public String getChecksum() {
{
return checksum; return checksum;
} }
@@ -390,8 +382,7 @@ public class BootstrapContextListener implements ServletContextListener
* *
* @return * @return
*/ */
public String getName() public String getName() {
{
return name; return name;
} }
@@ -415,4 +406,11 @@ public class BootstrapContextListener implements ServletContextListener
/** Field description */ /** Field description */
private boolean registered = false; private boolean registered = false;
private static class ScmContextListenerModule extends AbstractModule {
@Override
protected void configure() {
install(new FactoryModuleBuilder().build(ScmContextListener.Factory.class));
}
}
} }

View File

@@ -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 <T> void bind(Class<T> clazz, Class<? extends T> defaultImplementation) {
Class<? extends T> implementation = find(clazz, defaultImplementation);
LOG.debug("bind {} to {}", clazz, implementation);
bind(clazz).to(implementation);
}
private <T> Class<? extends T> find(Class<T> clazz, Class<? extends T> defaultImplementation) {
Class<? extends T> 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;
}
}

View File

@@ -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<UpdateStep> updateStepBinder = Multibinder.newSetBinder(binder(), UpdateStep.class);
pluginLoader
.getExtensionProcessor()
.byExtensionPoint(UpdateStep.class)
.forEach(stepClass -> updateStepBinder.addBinding().to(stepClass));
}
}

View File

@@ -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<UpdateStep> steps;
private final ConfigurationEntryStore<UpdateVersionInfo> store;
@Inject
public UpdateEngine(Set<UpdateStep> steps, ConfigurationEntryStoreFactory storeFactory) {
this.steps = sortSteps(steps);
this.store = storeFactory.withType(UpdateVersionInfo.class).withName(STORE_NAME).build();
}
private List<UpdateStep> sortSteps(Set<UpdateStep> steps) {
LOG.trace("sorting available update steps:");
List<UpdateStep> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -59,12 +59,15 @@ import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.HookFeature; import sonia.scm.repository.api.HookFeature;
import sonia.scm.repository.spi.HookContextProvider; import sonia.scm.repository.spi.HookContextProvider;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import sonia.scm.repository.xml.XmlRepositoryDAO; import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.security.DefaultKeyGenerator; import sonia.scm.security.DefaultKeyGenerator;
import sonia.scm.security.KeyGenerator; import sonia.scm.security.KeyGenerator;
import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.JAXBConfigurationStoreFactory; import sonia.scm.store.JAXBConfigurationStoreFactory;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@@ -86,451 +89,447 @@ import static org.mockito.Mockito.*;
password = "secret", password = "secret",
configuration = "classpath:sonia/scm/repository/shiro.ini" configuration = "classpath:sonia/scm/repository/shiro.ini"
) )
public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { public class DefaultRepositoryManagerTest {//extends ManagerTestBase<Repository> {
{
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<Repository> 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<String> 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<Repository> 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<RepositoryHandler> 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<HookFeature> 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<Repository> 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();
}
}
}
// {
// 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<Repository> 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<String> 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<Repository> 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<RepositoryHandler> 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<HookFeature> 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<Repository> 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();
// }
// }
// }
//
} }

View File

@@ -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<String> processedUpdates = new ArrayList<>();
@Test
void shouldProcessStepsInCorrectOrder() {
LinkedHashSet<UpdateStep> 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<UpdateStep> 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<UpdateStep> 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);
}
}
}