mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 00:15:44 +01:00
Merge with 2.0.0-m3
This commit is contained in:
14
pom.xml
14
pom.xml
@@ -409,8 +409,9 @@
|
||||
<plugin>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>buildfrontend-maven-plugin</artifactId>
|
||||
<version>2.2.0</version>
|
||||
<version>2.3.0</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
@@ -432,6 +433,12 @@
|
||||
<artifactId>enunciate-maven-plugin</artifactId>
|
||||
<version>${enunciate.version}</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>sonia.scm.maven</groupId>
|
||||
<artifactId>smp-maven-plugin</artifactId>
|
||||
<version>1.0.0-alpha-4</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
|
||||
@@ -838,8 +845,8 @@
|
||||
<quartz.version>2.2.3</quartz.version>
|
||||
|
||||
<!-- frontend -->
|
||||
<nodejs.version>8.11.4</nodejs.version>
|
||||
<yarn.version>1.9.4</yarn.version>
|
||||
<nodejs.version>10.16.0</nodejs.version>
|
||||
<yarn.version>1.16.0</yarn.version>
|
||||
|
||||
<!-- build properties -->
|
||||
<project.build.javaLevel>1.8</project.build.javaLevel>
|
||||
@@ -855,7 +862,6 @@
|
||||
<!-- *UserPassword JS files are excluded because extraction of common code would not make the code more readable -->
|
||||
<sonar.cpd.exclusions>**/*StoreFactory.java,**/*UserPassword.js</sonar.cpd.exclusions>
|
||||
|
||||
<node.version>8.11.4</node.version>
|
||||
<sonar.nodejs.executable>./scm-ui/target/frontend/buildfrontend-node/node-v${node.version}-linux-x64/bin/node</sonar.nodejs.executable>
|
||||
|
||||
|
||||
|
||||
@@ -3,11 +3,80 @@ 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();
|
||||
}
|
||||
|
||||
@@ -59,9 +59,6 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
|
||||
|
||||
public static final String DEFAULT_VERSION_INFORMATION = "unknown";
|
||||
|
||||
public static final String DOT = ".";
|
||||
static final String REPOSITORIES_NATIVE_DIRECTORY = "data";
|
||||
|
||||
/**
|
||||
* the logger for AbstractSimpleRepositoryHandler
|
||||
*/
|
||||
|
||||
@@ -42,6 +42,8 @@ import java.io.File;
|
||||
*/
|
||||
public interface RepositoryDirectoryHandler extends RepositoryHandler {
|
||||
|
||||
String REPOSITORIES_NATIVE_DIRECTORY = "data";
|
||||
|
||||
/**
|
||||
* Get the current directory of the repository for the given id.
|
||||
* @return the current directory of the given repository
|
||||
|
||||
@@ -57,7 +57,7 @@ import static java.util.Collections.unmodifiableSet;
|
||||
* Custom role with specific permissions related to {@link Repository}.
|
||||
* This object should be immutable, but could not be due to mapstruct.
|
||||
*/
|
||||
@StaticPermissions(value = "repositoryRole", permissions = {}, globalPermissions = {"read", "modify"})
|
||||
@StaticPermissions(value = "repositoryRole", permissions = {}, globalPermissions = {"write"})
|
||||
@XmlRootElement(name = "roles")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class RepositoryRole implements ModelObject, PermissionObject {
|
||||
@@ -121,7 +121,10 @@ public class RepositoryRole implements ModelObject, PermissionObject {
|
||||
* @return the hash code value for the {@link RepositoryRole}
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
public int hashCode()
|
||||
{
|
||||
// Normally we do not have a log of repository permissions having the same size of verbs, but different content.
|
||||
// Therefore we do not use the verbs themselves for the hash code but only the number of verbs.
|
||||
return Objects.hashCode(name, verbs == null? -1: verbs.size());
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,24 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject
|
||||
this.mail = mail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param name
|
||||
* @param displayName
|
||||
* @param mail
|
||||
*/
|
||||
public User(String name, String displayName, String mail, String password, String type, boolean active)
|
||||
{
|
||||
this.name = name;
|
||||
this.displayName = displayName;
|
||||
this.mail = mail;
|
||||
this.password = password;
|
||||
this.type = type;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.store.StoreConstants;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -28,9 +29,10 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Singleton
|
||||
public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver<Path> {
|
||||
|
||||
private static final String STORE_NAME = "repositories";
|
||||
public static final String STORE_NAME = "repository-paths";
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
private final InitialRepositoryLocationResolver initialRepositoryLocationResolver;
|
||||
@@ -48,7 +50,7 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
|
||||
this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC());
|
||||
}
|
||||
|
||||
public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) {
|
||||
PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) {
|
||||
super(Path.class);
|
||||
this.contextProvider = contextProvider;
|
||||
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
|
||||
@@ -138,4 +140,8 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
|
||||
.resolve(StoreConstants.CONFIG_DIRECTORY_NAME)
|
||||
.resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION));
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
this.read();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,10 +95,17 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
|
||||
@Override
|
||||
public void add(Repository repository) {
|
||||
add(repository, repositoryLocationResolver.create(repository.getId()));
|
||||
}
|
||||
|
||||
public void add(Repository repository, Object location) {
|
||||
if (!(location instanceof Path)) {
|
||||
throw new IllegalArgumentException("can only handle locations of type " + Path.class.getName() + ", not of type " + location.getClass().getName());
|
||||
}
|
||||
Repository clone = repository.clone();
|
||||
|
||||
synchronized (this) {
|
||||
Path repositoryPath = repositoryLocationResolver.create(repository.getId());
|
||||
Path repositoryPath = (Path) location;
|
||||
|
||||
try {
|
||||
Path metadataPath = resolveDataPath(repositoryPath);
|
||||
@@ -111,10 +118,8 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
byId.put(repository.getId(), clone);
|
||||
byNamespaceAndName.put(repository.getNamespaceAndName(), clone);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean contains(Repository repository) {
|
||||
return byId.containsKey(repository.getId());
|
||||
@@ -193,4 +198,11 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
public Long getLastModified() {
|
||||
return repositoryLocationResolver.getLastModified();
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
repositoryLocationResolver.refresh();
|
||||
byNamespaceAndName.clear();
|
||||
byId.clear();
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ class PathBasedRepositoryLocationResolverTest {
|
||||
}
|
||||
|
||||
private String getXmlFileContent() {
|
||||
Path storePath = basePath.resolve("config").resolve("repositories.xml");
|
||||
Path storePath = basePath.resolve("config").resolve("repository-paths.xml");
|
||||
|
||||
assertThat(storePath).isRegularFile();
|
||||
return content(storePath);
|
||||
|
||||
@@ -8,8 +8,6 @@ import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
@@ -32,7 +30,9 @@ import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -47,9 +47,6 @@ class XmlRepositoryDAOTest {
|
||||
@Mock
|
||||
private PathBasedRepositoryLocationResolver locationResolver;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<BiConsumer<String, Path>> forAllCaptor;
|
||||
|
||||
private FileSystem fileSystem = new DefaultFileSystem();
|
||||
|
||||
private XmlRepositoryDAO dao;
|
||||
@@ -268,22 +265,6 @@ class XmlRepositoryDAOTest {
|
||||
|
||||
verify(locationResolver).updateModificationDate();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadExistingRepositoriesFromPathDatabase(@TempDirectory.TempDir Path basePath) throws IOException {
|
||||
doNothing().when(locationResolver).forAllPaths(forAllCaptor.capture());
|
||||
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
|
||||
|
||||
Path repositoryPath = basePath.resolve("existing");
|
||||
Files.createDirectories(repositoryPath);
|
||||
URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml");
|
||||
Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml"));
|
||||
|
||||
forAllCaptor.getValue().accept("existing", repositoryPath);
|
||||
|
||||
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
|
||||
}
|
||||
|
||||
private String getXmlFileContent(String id) {
|
||||
Path storePath = metadataFile(id);
|
||||
@@ -303,8 +284,61 @@ class XmlRepositoryDAOTest {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Repository createRepository(String id) {
|
||||
@Nested
|
||||
class WithExistingRepositories {
|
||||
|
||||
private Path repositoryPath;
|
||||
|
||||
@BeforeEach
|
||||
void createMetadataFileForRepository(@TempDirectory.TempDir Path basePath) throws IOException {
|
||||
repositoryPath = basePath.resolve("existing");
|
||||
|
||||
Files.createDirectories(repositoryPath);
|
||||
URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml");
|
||||
Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadExistingRepositoriesFromPathDatabase() {
|
||||
// given
|
||||
mockExistingPath();
|
||||
|
||||
// when
|
||||
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
|
||||
|
||||
// then
|
||||
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRefreshWithExistingRepositoriesFromPathDatabase() {
|
||||
// given
|
||||
doNothing().when(locationResolver).forAllPaths(any());
|
||||
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
|
||||
|
||||
mockExistingPath();
|
||||
|
||||
// when
|
||||
dao.refresh();
|
||||
|
||||
// then
|
||||
verify(locationResolver).refresh();
|
||||
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
|
||||
}
|
||||
|
||||
private void mockExistingPath() {
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
((BiConsumer<String, Path>) invocation.getArgument(0)).accept("existing", repositoryPath);
|
||||
return null;
|
||||
}
|
||||
).when(locationResolver).forAllPaths(any());
|
||||
}
|
||||
}
|
||||
|
||||
private Repository createRepository(String id) {
|
||||
return new Repository(id, "xml", "space", id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
<plugin>
|
||||
<groupId>sonia.scm.maven</groupId>
|
||||
<artifactId>smp-maven-plugin</artifactId>
|
||||
<version>1.0.0-alpha-3</version>
|
||||
<extensions>true</extensions>
|
||||
</plugin>
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"@scm-manager/ui-extensions": "^0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.28"
|
||||
"@scm-manager/ui-bundler": "^0.0.29"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.web.lfs;
|
||||
|
||||
import com.github.legman.Subscribe;
|
||||
import com.google.inject.Inject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.EagerSingleton;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.repository.GitRepositoryHandler;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryEvent;
|
||||
import sonia.scm.store.Blob;
|
||||
import sonia.scm.store.BlobStore;
|
||||
|
||||
/**
|
||||
* Listener which removes all lfs objects from a blob store, whenever its corresponding git repository gets deleted.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.54
|
||||
*/
|
||||
@Extension
|
||||
@EagerSingleton
|
||||
public class LfsStoreRemoveListener {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LfsBlobStoreFactory.class);
|
||||
|
||||
private final LfsBlobStoreFactory lfsBlobStoreFactory;
|
||||
|
||||
@Inject
|
||||
public LfsStoreRemoveListener(LfsBlobStoreFactory lfsBlobStoreFactory) {
|
||||
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all object from the blob store, if the event is an delete event and the repository is a git repository.
|
||||
*
|
||||
* @param event repository event
|
||||
*/
|
||||
@Subscribe
|
||||
public void handleRepositoryEvent(RepositoryEvent event) {
|
||||
if ( isDeleteEvent(event) && isGitRepositoryEvent(event) ) {
|
||||
removeLfsStore(event.getItem());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDeleteEvent(RepositoryEvent event) {
|
||||
return HandlerEventType.DELETE == event.getEventType();
|
||||
}
|
||||
|
||||
private boolean isGitRepositoryEvent(RepositoryEvent event) {
|
||||
return event.getItem() != null
|
||||
&& event.getItem().getType().equals(GitRepositoryHandler.TYPE_NAME);
|
||||
}
|
||||
|
||||
private void removeLfsStore(Repository repository) {
|
||||
LOG.debug("remove all blobs from store, because corresponding git repository {} was removed", repository.getName());
|
||||
BlobStore blobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
|
||||
for ( Blob blob : blobStore.getAll() ) {
|
||||
LOG.trace("remove blob {}, because repository {} was removed", blob.getId(), repository.getName());
|
||||
blobStore.remove(blob);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -117,6 +117,6 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
||||
|
||||
initRepository();
|
||||
File path = repositoryHandler.getDirectory(repository.getId());
|
||||
assertEquals(repoPath.toString() + File.separator + AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||
assertEquals(repoPath.toString() + File.separator + RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.web.lfs;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import java.util.List;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import static org.mockito.Mockito.*;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryEvent;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.store.Blob;
|
||||
import sonia.scm.store.BlobStore;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link LfsStoreRemoveListener}.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class LfsStoreRemoveListenerTest {
|
||||
|
||||
@Mock
|
||||
private LfsBlobStoreFactory lfsBlobStoreFactory;
|
||||
|
||||
@Mock
|
||||
private BlobStore blobStore;
|
||||
|
||||
@InjectMocks
|
||||
private LfsStoreRemoveListener lfsStoreRemoveListener;
|
||||
|
||||
@Test
|
||||
public void testHandleRepositoryEventWithNonDeleteEvents() {
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_CREATE));
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.CREATE));
|
||||
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_MODIFY));
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.MODIFY));
|
||||
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_DELETE));
|
||||
|
||||
verifyZeroInteractions(lfsBlobStoreFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleRepositoryEventWithNonGitRepositories() {
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "svn"));
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "hg"));
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "dummy"));
|
||||
|
||||
verifyZeroInteractions(lfsBlobStoreFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleRepositoryEvent() {
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold("git");
|
||||
|
||||
when(lfsBlobStoreFactory.getLfsBlobStore(heartOfGold)).thenReturn(blobStore);
|
||||
Blob blobA = mockBlob("a");
|
||||
Blob blobB = mockBlob("b");
|
||||
List<Blob> blobs = Lists.newArrayList(blobA, blobB);
|
||||
when(blobStore.getAll()).thenReturn(blobs);
|
||||
|
||||
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(new RepositoryEvent(HandlerEventType.DELETE, heartOfGold));
|
||||
verify(blobStore).getAll();
|
||||
verify(blobStore).remove(blobA);
|
||||
verify(blobStore).remove(blobB);
|
||||
|
||||
verifyNoMoreInteractions(blobStore);
|
||||
}
|
||||
|
||||
private Blob mockBlob(String id) {
|
||||
Blob blob = mock(Blob.class);
|
||||
when(blob.getId()).thenReturn(id);
|
||||
return blob;
|
||||
}
|
||||
|
||||
private RepositoryEvent event(HandlerEventType eventType) {
|
||||
return event(eventType, "git");
|
||||
}
|
||||
|
||||
private RepositoryEvent event(HandlerEventType eventType, String repositoryType) {
|
||||
return new RepositoryEvent(eventType, RepositoryTestData.create42Puzzle(repositoryType));
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,6 @@
|
||||
"@scm-manager/ui-extensions": "^0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.28"
|
||||
"@scm-manager/ui-bundler": "^0.0.29"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,6 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
||||
|
||||
initRepository();
|
||||
File path = repositoryHandler.getDirectory(repository.getId());
|
||||
assertEquals(repoPath.toString() + File.separator + AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||
assertEquals(repoPath.toString() + File.separator + RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,6 @@
|
||||
"@scm-manager/ui-extensions": "^0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.28"
|
||||
"@scm-manager/ui-bundler": "^0.0.29"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,6 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
||||
|
||||
initRepository();
|
||||
File path = repositoryHandler.getDirectory(repository.getId());
|
||||
assertEquals(repoPath.toString()+File.separator+ AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||
assertEquals(repoPath.toString()+File.separator+ RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -113,7 +113,7 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase {
|
||||
File repoDirectory = new File(baseDirectory, repository.getId());
|
||||
repoPath = repoDirectory.toPath();
|
||||
// when(repoDao.getPath(repository.getId())).thenReturn(repoPath);
|
||||
return new File(repoDirectory, AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||
return new File(repoDirectory, RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||
}
|
||||
|
||||
protected File baseDirectory;
|
||||
|
||||
@@ -153,7 +153,12 @@ public abstract class ZippedRepositoryTestBase extends AbstractTestBase
|
||||
*/
|
||||
private void extract(File folder) throws IOException
|
||||
{
|
||||
URL url = Resources.getResource(getZippedRepositoryResource());
|
||||
String zippedRepositoryResource = getZippedRepositoryResource();
|
||||
extract(folder, zippedRepositoryResource);
|
||||
}
|
||||
|
||||
public static void extract(File targetFolder, String zippedRepositoryResource) throws IOException {
|
||||
URL url = Resources.getResource(zippedRepositoryResource);
|
||||
ZipInputStream zip = null;
|
||||
|
||||
try
|
||||
@@ -164,7 +169,7 @@ public abstract class ZippedRepositoryTestBase extends AbstractTestBase
|
||||
|
||||
while (entry != null)
|
||||
{
|
||||
File file = new File(folder, entry.getName());
|
||||
File file = new File(targetFolder, entry.getName());
|
||||
File parent = file.getParentFile();
|
||||
|
||||
if (!parent.exists())
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"eslint-fix": "eslint src --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.28",
|
||||
"@scm-manager/ui-bundler": "^0.0.29",
|
||||
"create-index": "^2.3.0",
|
||||
"enzyme": "^3.5.0",
|
||||
"enzyme-adapter-react-16": "^1.3.1",
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import { withContextPath } from "./urls";
|
||||
|
||||
/**
|
||||
* Adds anchor links to markdown headings.
|
||||
*
|
||||
* @see <a href="https://github.com/rexxars/react-markdown/issues/69">Headings are missing anchors / ids</a>
|
||||
*/
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
level: number,
|
||||
location: any
|
||||
};
|
||||
|
||||
function flatten(text: string, child: any) {
|
||||
return typeof child === "string"
|
||||
? text + child
|
||||
: React.Children.toArray(child.props.children).reduce(flatten, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns heading text into a anchor id
|
||||
*
|
||||
* @VisibleForTesting
|
||||
*/
|
||||
export function headingToAnchorId(heading: string) {
|
||||
return heading.toLowerCase().replace(/\W/g, "-");
|
||||
}
|
||||
|
||||
function MarkdownHeadingRenderer(props: Props) {
|
||||
const children = React.Children.toArray(props.children);
|
||||
const heading = children.reduce(flatten, "");
|
||||
const anchorId = headingToAnchorId(heading);
|
||||
const headingElement = React.createElement("h" + props.level, {}, props.children);
|
||||
const href = withContextPath(props.location.pathname + "#" + anchorId);
|
||||
|
||||
return (
|
||||
<a id={`${anchorId}`} className="anchor" href={href}>
|
||||
{headingElement}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(MarkdownHeadingRenderer);
|
||||
@@ -0,0 +1,18 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { headingToAnchorId } from "./MarkdownHeadingRenderer";
|
||||
|
||||
describe("headingToAnchorId tests", () => {
|
||||
|
||||
it("should lower case the text", () => {
|
||||
expect(headingToAnchorId("Hello")).toBe("hello");
|
||||
expect(headingToAnchorId("HeLlO")).toBe("hello");
|
||||
expect(headingToAnchorId("HELLO")).toBe("hello");
|
||||
});
|
||||
|
||||
it("should replace spaces with hyphen", () => {
|
||||
expect(headingToAnchorId("awesome stuff")).toBe("awesome-stuff");
|
||||
expect(headingToAnchorId("a b c d e f")).toBe("a-b-c-d-e-f");
|
||||
});
|
||||
|
||||
});
|
||||
@@ -3,17 +3,49 @@ import React from "react";
|
||||
import SyntaxHighlighter from "./SyntaxHighlighter";
|
||||
import Markdown from "react-markdown/with-html";
|
||||
import {binder} from "@scm-manager/ui-extensions";
|
||||
import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer";
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
|
||||
type Props = {
|
||||
content: string,
|
||||
renderContext?: Object,
|
||||
renderers?: Object,
|
||||
enableAnchorHeadings: boolean,
|
||||
|
||||
// context props
|
||||
location: any
|
||||
};
|
||||
|
||||
class MarkdownView extends React.Component<Props> {
|
||||
|
||||
static defaultProps = {
|
||||
enableAnchorHeadings: false
|
||||
};
|
||||
|
||||
contentRef: ?HTMLDivElement;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// we have to use componentDidUpdate, because we have to wait until all
|
||||
// children are rendered and componentDidMount is called before the
|
||||
// markdown content was rendered.
|
||||
const hash = this.props.location.hash;
|
||||
if (this.contentRef && hash) {
|
||||
// we query only child elements, to avoid strange scrolling with multiple
|
||||
// markdown elements on one page.
|
||||
const element = this.contentRef.querySelector(hash);
|
||||
if (element && element.scrollIntoView) {
|
||||
element.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {content, renderers, renderContext} = this.props;
|
||||
const {content, renderers, renderContext, enableAnchorHeadings} = this.props;
|
||||
|
||||
const rendererFactory = binder.getExtension("markdown-renderer-factory");
|
||||
let rendererList = renderers;
|
||||
@@ -26,11 +58,16 @@ class MarkdownView extends React.Component<Props> {
|
||||
rendererList = {};
|
||||
}
|
||||
|
||||
if (enableAnchorHeadings) {
|
||||
rendererList.heading = MarkdownHeadingRenderer;
|
||||
}
|
||||
|
||||
if (!rendererList.code){
|
||||
rendererList.code = SyntaxHighlighter;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={el => (this.contentRef = el)}>
|
||||
<Markdown
|
||||
className="content"
|
||||
skipHtml={true}
|
||||
@@ -38,8 +75,9 @@ class MarkdownView extends React.Component<Props> {
|
||||
source={content}
|
||||
renderers={rendererList}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MarkdownView;
|
||||
export default withRouter(MarkdownView);
|
||||
|
||||
@@ -17,13 +17,25 @@ const styles = {
|
||||
panel: {
|
||||
fontSize: "1rem"
|
||||
},
|
||||
/* breaks into a second row
|
||||
when buttons and title become too long */
|
||||
level: {
|
||||
flexWrap: "wrap"
|
||||
},
|
||||
titleHeader: {
|
||||
display: "flex",
|
||||
maxWidth: "100%",
|
||||
cursor: "pointer"
|
||||
},
|
||||
title: {
|
||||
marginLeft: ".25rem",
|
||||
fontSize: "1rem"
|
||||
},
|
||||
/* align child to right */
|
||||
buttonHeader: {
|
||||
display: "flex",
|
||||
marginLeft: "auto"
|
||||
},
|
||||
hunkDivider: {
|
||||
margin: ".5rem 0"
|
||||
},
|
||||
@@ -143,14 +155,41 @@ class DiffFile extends React.Component<Props, State> {
|
||||
return file.newPath;
|
||||
};
|
||||
|
||||
hoverFileTitle = (file: any) => {
|
||||
if (
|
||||
file.oldPath !== file.newPath &&
|
||||
(file.type === "copy" || file.type === "rename")
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
{file.oldPath} > {file.newPath}
|
||||
</>
|
||||
);
|
||||
} else if (file.type === "delete") {
|
||||
return file.oldPath;
|
||||
}
|
||||
return file.newPath;
|
||||
};
|
||||
|
||||
renderChangeTag = (file: any) => {
|
||||
const { t } = this.props;
|
||||
const { t, classes } = this.props;
|
||||
const key = "diff.changes." + file.type;
|
||||
let value = t(key);
|
||||
if (key === value) {
|
||||
value = file.type;
|
||||
}
|
||||
return <span className="tag is-info has-text-weight-normal">{value}</span>;
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
"tag",
|
||||
"is-info",
|
||||
"has-text-weight-normal",
|
||||
classes.changeType
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -187,20 +226,21 @@ class DiffFile extends React.Component<Props, State> {
|
||||
return (
|
||||
<div className={classNames("panel", classes.panel)}>
|
||||
<div className="panel-heading">
|
||||
<div className="level">
|
||||
<div className={classNames("level", classes.level)}>
|
||||
<div
|
||||
className={classNames("level-left", classes.titleHeader)}
|
||||
onClick={this.toggleCollapse}
|
||||
title={this.hoverFileTitle(file)}
|
||||
>
|
||||
<i className={icon} />
|
||||
<span className={classes.title}>
|
||||
<span
|
||||
className={classNames("is-ellipsis-overflow", classes.title)}
|
||||
>
|
||||
{this.renderFileTitle(file)}
|
||||
</span>
|
||||
<span className={classes.changeType}>
|
||||
{this.renderChangeTag(file)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<div className={classNames("level-right", classes.buttonHeader)}>
|
||||
<Button action={this.toggleSideBySide} className="reduced-mobile">
|
||||
<span className="icon is-small">
|
||||
<i
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@
|
||||
"check": "flow check"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.28"
|
||||
"@scm-manager/ui-bundler": "^0.0.29"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -54,7 +54,7 @@
|
||||
"pre-commit": "jest && flow && eslint src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.28",
|
||||
"@scm-manager/ui-bundler": "^0.0.29",
|
||||
"concat": "^1.0.3",
|
||||
"copyfiles": "^2.0.0",
|
||||
"enzyme": "^3.3.0",
|
||||
|
||||
@@ -9,29 +9,35 @@
|
||||
"repositoryRole": {
|
||||
"navLink": "Berechtigungsrollen",
|
||||
"title": "Berechtigungsrollen",
|
||||
"errorTitle": "Fehler",
|
||||
"errorSubtitle": "Unbekannter Berechtigungsrollen Fehler",
|
||||
"createSubtitle": "Berechtigungsrolle erstellen",
|
||||
"editSubtitle": "Berechtigungsrolle bearbeiten",
|
||||
"overview": {
|
||||
"title": "Übersicht aller verfügbaren Berechtigungsrollen",
|
||||
"noPermissionRoles": "Keine Berechtigungsrollen gefunden.",
|
||||
"system": "System",
|
||||
"createButton": "Berechtigungsrolle erstellen",
|
||||
"createButton": "Berechtigungsrolle erstellen"
|
||||
},
|
||||
"editButton": "Bearbeiten",
|
||||
"name": "Name",
|
||||
"type": "Typ",
|
||||
"verbs": "Verben",
|
||||
"button": {
|
||||
"edit": "Bearbeiten"
|
||||
},
|
||||
"create": {
|
||||
"name": "Name"
|
||||
},
|
||||
"edit": "Berechtigungsrolle bearbeiten",
|
||||
"verbs": "Berechtigungen",
|
||||
"system": "System",
|
||||
"form": {
|
||||
"subtitle": "Berechtigungsrolle bearbeiten",
|
||||
"name": "Name",
|
||||
"permissions": "Berechtigungen",
|
||||
"submit": "Speichern"
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
"name": "Name",
|
||||
"system": "System"
|
||||
"deleteRole" : {
|
||||
"button": "Löschen",
|
||||
"subtitle": "Berechtigungsrolle löschen",
|
||||
"confirmAlert": {
|
||||
"title": "Berechtigungsrolle löschen",
|
||||
"message": "Soll die Berechtigungsrolle wirklich gelöscht werden? Alle Nutzer dieser Rolle verlieren Ihre Berechtigungen.",
|
||||
"submit": "Ja",
|
||||
"cancel": "Nein"
|
||||
}
|
||||
},
|
||||
"config-form": {
|
||||
"submit": "Speichern",
|
||||
|
||||
@@ -9,29 +9,35 @@
|
||||
"repositoryRole": {
|
||||
"navLink": "Permission Roles",
|
||||
"title": "Permission Roles",
|
||||
"errorTitle": "Error",
|
||||
"errorSubtitle": "Unknown Permission Role Error",
|
||||
"createSubtitle": "Create Permission Role",
|
||||
"editSubtitle": "Edit Permission Role",
|
||||
"overview": {
|
||||
"title": "Overview of all permission roles",
|
||||
"noPermissionRoles": "No permission roles found.",
|
||||
"system": "System",
|
||||
"createButton": "Create Permission Role",
|
||||
"createButton": "Create Permission Role"
|
||||
},
|
||||
"editButton": "Edit",
|
||||
"name": "Name",
|
||||
"type": "Type",
|
||||
"verbs": "Verbs",
|
||||
"edit": "Edit Permission Role",
|
||||
"button": {
|
||||
"edit": "Edit"
|
||||
},
|
||||
"create": {
|
||||
"name": "Name"
|
||||
},
|
||||
"verbs": "Permissions",
|
||||
"system": "System",
|
||||
"form": {
|
||||
"subtitle": "Edit Permission Role",
|
||||
"name": "Name",
|
||||
"permissions": "Permissions",
|
||||
"submit": "Save"
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
"name": "Name",
|
||||
"system": "System"
|
||||
"deleteRole": {
|
||||
"button": "Delete",
|
||||
"subtitle": "Delete Permission Role",
|
||||
"confirmAlert": {
|
||||
"title": "Delete Permission Role",
|
||||
"message": "Do you really want to delete this permission role? All users who own this role will lose their permissions.",
|
||||
"submit": "Yes",
|
||||
"cancel": "No"
|
||||
}
|
||||
},
|
||||
"config-form": {
|
||||
"submit": "Submit",
|
||||
|
||||
@@ -74,7 +74,6 @@ class Config extends React.Component<Props> {
|
||||
path={`${url}/roles/create`}
|
||||
render={() => (
|
||||
<CreateRepositoryRole
|
||||
disabled={false}
|
||||
history={this.props.history}
|
||||
/>
|
||||
)}
|
||||
@@ -104,6 +103,7 @@ class Config extends React.Component<Props> {
|
||||
to={`${url}/roles/`}
|
||||
label={t("repositoryRole.navLink")}
|
||||
activeWhenMatch={this.matchesRoles}
|
||||
activeOnlyWhenExact={false}
|
||||
/>
|
||||
<ExtensionPoint
|
||||
name="config.navigation"
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||
import { translate } from "react-i18next";
|
||||
import { compose } from "redux";
|
||||
import injectSheet from "react-jss";
|
||||
import { translate } from "react-i18next";
|
||||
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
role: RepositoryRole,
|
||||
|
||||
// context props
|
||||
classes: any,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||
import ExtensionPoint from "@scm-manager/ui-extensions/lib/ExtensionPoint";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import PermissionRoleDetailsTable from "./PermissionRoleDetailsTable";
|
||||
import { Button, Subtitle } from "@scm-manager/ui-components";
|
||||
import { Button } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
role: RepositoryRole,
|
||||
@@ -20,7 +20,7 @@ class PermissionRoleDetails extends React.Component<Props> {
|
||||
if (!!this.props.role._links.update) {
|
||||
return (
|
||||
<Button
|
||||
label={t("repositoryRole.button.edit")}
|
||||
label={t("repositoryRole.editButton")}
|
||||
link={`${url}/edit`}
|
||||
color="primary"
|
||||
/>
|
||||
@@ -33,18 +33,16 @@ class PermissionRoleDetails extends React.Component<Props> {
|
||||
const { role } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<PermissionRoleDetailsTable role={role} />
|
||||
<hr />
|
||||
{this.renderEditButton()}
|
||||
<div className="content">
|
||||
<ExtensionPoint
|
||||
name="repositoryRole.role-details.information"
|
||||
renderAll={true}
|
||||
props={{ role }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import AvailableVerbs from "./AvailableVerbs";
|
||||
|
||||
type Props = {
|
||||
role: RepositoryRole,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ type Props = {
|
||||
baseUrl: string,
|
||||
roles: RepositoryRole[],
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
@@ -18,7 +19,7 @@ class PermissionRoleTable extends React.Component<Props> {
|
||||
<table className="card-table table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("repositoryRole.form.name")}</th>
|
||||
<th>{t("repositoryRole.name")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
system?: boolean,
|
||||
|
||||
// context props
|
||||
classes: any,
|
||||
t: string => string
|
||||
};
|
||||
@@ -24,7 +26,7 @@ class SystemRoleTag extends React.Component<Props> {
|
||||
if (system) {
|
||||
return (
|
||||
<span className={classNames("tag is-dark", classes.tag)}>
|
||||
{t("role.system")}
|
||||
{t("repositoryRole.system")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
import RepositoryRoleForm from "./RepositoryRoleForm";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
import { ErrorNotification, Title } from "@scm-manager/ui-components";
|
||||
import {ErrorNotification, Subtitle, Title} from "@scm-manager/ui-components";
|
||||
import {
|
||||
createRole,
|
||||
getCreateRoleFailure,
|
||||
@@ -15,11 +15,12 @@ import {
|
||||
getRepositoryRolesLink,
|
||||
getRepositoryVerbsLink
|
||||
} from "../../../modules/indexResource";
|
||||
import type {History} from "history";
|
||||
|
||||
type Props = {
|
||||
disabled: boolean,
|
||||
repositoryRolesLink: string,
|
||||
error?: Error,
|
||||
history: History,
|
||||
|
||||
//dispatch function
|
||||
addRole: (link: string, role: RepositoryRole, callback?: () => void) => void,
|
||||
@@ -50,8 +51,8 @@ class CreateRepositoryRole extends React.Component<Props> {
|
||||
return (
|
||||
<>
|
||||
<Title title={t("repositoryRole.title")} />
|
||||
<Subtitle subtitle={t("repositoryRole.createSubtitle")} />
|
||||
<RepositoryRoleForm
|
||||
disabled={this.props.disabled}
|
||||
submitForm={role => this.createRepositoryRole(role)}
|
||||
/>
|
||||
</>
|
||||
|
||||
113
scm-ui/src/config/roles/containers/DeleteRepositoryRole.js
Normal file
113
scm-ui/src/config/roles/containers/DeleteRepositoryRole.js
Normal file
@@ -0,0 +1,113 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||
import {
|
||||
Subtitle,
|
||||
DeleteButton,
|
||||
confirmAlert,
|
||||
ErrorNotification
|
||||
} from "@scm-manager/ui-components";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import type { History } from "history";
|
||||
import {
|
||||
deleteRole,
|
||||
getDeleteRoleFailure,
|
||||
isDeleteRolePending
|
||||
} from "../modules/roles";
|
||||
|
||||
type Props = {
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
role: RepositoryRole,
|
||||
confirmDialog?: boolean,
|
||||
deleteRole: (role: RepositoryRole, callback?: () => void) => void,
|
||||
|
||||
// context props
|
||||
history: History,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class DeleteRepositoryRole extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
confirmDialog: true
|
||||
};
|
||||
|
||||
roleDeleted = () => {
|
||||
this.props.history.push("/config/roles/");
|
||||
};
|
||||
|
||||
deleteRole = () => {
|
||||
this.props.deleteRole(this.props.role, this.roleDeleted);
|
||||
};
|
||||
|
||||
confirmDelete = () => {
|
||||
const { t } = this.props;
|
||||
confirmAlert({
|
||||
title: t("deleteRole.confirmAlert.title"),
|
||||
message: t("deleteRole.confirmAlert.message"),
|
||||
buttons: [
|
||||
{
|
||||
label: t("deleteRole.confirmAlert.submit"),
|
||||
onClick: () => this.deleteRole()
|
||||
},
|
||||
{
|
||||
label: t("deleteRole.confirmAlert.cancel"),
|
||||
onClick: () => null
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
isDeletable = () => {
|
||||
return this.props.role._links.delete;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, error, confirmDialog, t } = this.props;
|
||||
const action = confirmDialog ? this.confirmDelete : this.deleteRole;
|
||||
|
||||
if (!this.isDeletable()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle subtitle={t("deleteRole.subtitle")} />
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<ErrorNotification error={error} />
|
||||
<DeleteButton
|
||||
label={t("deleteRole.button")}
|
||||
action={action}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const loading = isDeleteRolePending(state, ownProps.role.name);
|
||||
const error = getDeleteRoleFailure(state, ownProps.role.name);
|
||||
return {
|
||||
loading,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
deleteRole: (role: RepositoryRole, callback?: () => void) => {
|
||||
dispatch(deleteRole(role, callback));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withRouter(translate("config")(DeleteRepositoryRole)));
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
isModifyRolePending,
|
||||
modifyRole
|
||||
} from "../modules/roles";
|
||||
import { ErrorNotification } from "@scm-manager/ui-components";
|
||||
import { ErrorNotification, Subtitle } from "@scm-manager/ui-components";
|
||||
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||
import type { History } from "history";
|
||||
import DeleteRepositoryRole from "./DeleteRepositoryRole";
|
||||
|
||||
type Props = {
|
||||
disabled: boolean,
|
||||
@@ -17,26 +19,25 @@ type Props = {
|
||||
repositoryRolesLink: string,
|
||||
error?: Error,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
history: History,
|
||||
|
||||
//dispatch function
|
||||
updateRole: (
|
||||
link: string,
|
||||
role: RepositoryRole,
|
||||
callback?: () => void
|
||||
) => void
|
||||
updateRole: (role: RepositoryRole, callback?: () => void) => void
|
||||
};
|
||||
|
||||
class EditRepositoryRole extends React.Component<Props> {
|
||||
repositoryRoleUpdated = (role: RepositoryRole) => {
|
||||
const { history } = this.props;
|
||||
history.push("/config/roles/");
|
||||
repositoryRoleUpdated = () => {
|
||||
this.props.history.push("/config/roles/");
|
||||
};
|
||||
|
||||
updateRepositoryRole = (role: RepositoryRole) => {
|
||||
this.props.updateRole(role, () => this.repositoryRoleUpdated(role));
|
||||
this.props.updateRole(role, this.repositoryRoleUpdated);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { error } = this.props;
|
||||
const { error, t } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
@@ -44,18 +45,20 @@ class EditRepositoryRole extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle subtitle={t("repositoryRole.editSubtitle")} />
|
||||
<RepositoryRoleForm
|
||||
nameDisabled={true}
|
||||
role={this.props.role}
|
||||
submitForm={role => this.updateRepositoryRole(role)}
|
||||
/>
|
||||
<hr/>
|
||||
<DeleteRepositoryRole role={this.props.role}/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const loading = isModifyRolePending(state);
|
||||
const loading = isModifyRolePending(state, ownProps.role.name);
|
||||
const error = getModifyRoleFailure(state, ownProps.role.name);
|
||||
|
||||
return {
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
type Props = {
|
||||
role?: RepositoryRole,
|
||||
loading?: boolean,
|
||||
nameDisabled: boolean,
|
||||
availableVerbs: string[],
|
||||
verbsLink: string,
|
||||
submitForm: RepositoryRole => void,
|
||||
@@ -103,7 +102,7 @@ class RepositoryRoleForm extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, availableVerbs, nameDisabled, t } = this.props;
|
||||
const { loading, availableVerbs, t } = this.props;
|
||||
const { role } = this.state;
|
||||
|
||||
const verbSelectBoxes = !availableVerbs
|
||||
@@ -119,28 +118,25 @@ class RepositoryRoleForm extends React.Component<Props, State> {
|
||||
|
||||
return (
|
||||
<form onSubmit={this.submit}>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<InputField
|
||||
name="name"
|
||||
label={t("repositoryRole.create.name")}
|
||||
label={t("repositoryRole.form.name")}
|
||||
onChange={this.handleNameChange}
|
||||
value={role.name ? role.name : ""}
|
||||
disabled={nameDisabled}
|
||||
disabled={!!this.props.role}
|
||||
/>
|
||||
<div className="field">
|
||||
<label className="label">
|
||||
{t("repositoryRole.form.permissions")}
|
||||
</label>
|
||||
{verbSelectBoxes}
|
||||
</div>
|
||||
</div>
|
||||
<>{verbSelectBoxes}</>
|
||||
<hr />
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<SubmitButton
|
||||
loading={loading}
|
||||
label={t("repositoryRole.form.submit")}
|
||||
disabled={!this.isValid()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { History } from "history";
|
||||
import type { RepositoryRole, PagedCollection } from "@scm-manager/ui-types";
|
||||
import {
|
||||
Title,
|
||||
Subtitle,
|
||||
Loading,
|
||||
Notification,
|
||||
LinkPaginator,
|
||||
@@ -22,7 +23,8 @@ import {
|
||||
getFetchRolesFailure
|
||||
} from "../modules/roles";
|
||||
import PermissionRoleTable from "../components/PermissionRoleTable";
|
||||
import { getRolesLink } from "../../../modules/indexResource";
|
||||
import { getRepositoryRolesLink } from "../../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
baseUrl: string,
|
||||
roles: RepositoryRole[],
|
||||
@@ -36,6 +38,7 @@ type Props = {
|
||||
// context objects
|
||||
t: string => string,
|
||||
history: History,
|
||||
location: any,
|
||||
|
||||
// dispatch functions
|
||||
fetchRolesByPage: (link: string, page: number) => void
|
||||
@@ -61,8 +64,7 @@ class RepositoryRoles extends React.Component<Props> {
|
||||
if (page !== statePage || prevProps.location.search !== location.search) {
|
||||
fetchRolesByPage(
|
||||
rolesLink,
|
||||
page,
|
||||
urls.getQueryStringFromLocation(location)
|
||||
page
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -76,11 +78,12 @@ class RepositoryRoles extends React.Component<Props> {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<Title title={t("repositoryRole.title")} />
|
||||
<Subtitle subtitle={t("repositoryRole.overview.title")} />
|
||||
{this.renderPermissionsTable()}
|
||||
{this.renderCreateButton()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,7 +99,7 @@ class RepositoryRoles extends React.Component<Props> {
|
||||
}
|
||||
return (
|
||||
<Notification type="info">
|
||||
{t("repositoryRole.noPermissionRoles")}
|
||||
{t("repositoryRole.overview.noPermissionRoles")}
|
||||
</Notification>
|
||||
);
|
||||
}
|
||||
@@ -106,7 +109,7 @@ class RepositoryRoles extends React.Component<Props> {
|
||||
if (canAddRoles) {
|
||||
return (
|
||||
<CreateButton
|
||||
label={t("repositoryRole.createButton")}
|
||||
label={t("repositoryRole.overview.createButton")}
|
||||
link={`${baseUrl}/create`}
|
||||
/>
|
||||
);
|
||||
@@ -123,7 +126,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
const page = urls.getPageFromMatch(match);
|
||||
const canAddRoles = isPermittedToCreateRoles(state);
|
||||
const list = selectListAsCollection(state);
|
||||
const rolesLink = getRolesLink(state);
|
||||
const rolesLink = getRepositoryRolesLink(state);
|
||||
|
||||
return {
|
||||
roles,
|
||||
|
||||
@@ -81,8 +81,6 @@ class SingleRepositoryRole extends React.Component<Props> {
|
||||
return (
|
||||
<>
|
||||
<Title title={t("repositoryRole.title")} />
|
||||
<div className="columns">
|
||||
<div className="column is-three-quarters">
|
||||
<Route
|
||||
path={`${url}/info`}
|
||||
component={() => <PermissionRoleDetail role={role} url={url} />}
|
||||
@@ -99,8 +97,6 @@ class SingleRepositoryRole extends React.Component<Props> {
|
||||
props={extensionProps}
|
||||
renderAll={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export class DeleteGroup extends React.Component<Props> {
|
||||
};
|
||||
|
||||
groupDeleted = () => {
|
||||
this.props.history.push("/groups");
|
||||
this.props.history.push("/groups/");
|
||||
};
|
||||
|
||||
confirmDelete = () => {
|
||||
|
||||
@@ -159,10 +159,6 @@ export function getSvnConfigLink(state: Object) {
|
||||
return getLink(state, "svnConfig");
|
||||
}
|
||||
|
||||
export function getRolesLink(state: Object) {
|
||||
return getLink(state, "repositoryRoles");
|
||||
}
|
||||
|
||||
export function getUserAutoCompleteLink(state: Object): string {
|
||||
const link = getLinkCollection(state, "autocomplete").find(
|
||||
i => i.name === "users"
|
||||
|
||||
@@ -35,7 +35,7 @@ class DeleteRepo extends React.Component<Props> {
|
||||
};
|
||||
|
||||
deleted = () => {
|
||||
this.props.history.push("/repos");
|
||||
this.props.history.push("/repos/");
|
||||
};
|
||||
|
||||
deleteRepo = () => {
|
||||
|
||||
@@ -161,7 +161,7 @@ class Permissions extends React.Component<Props> {
|
||||
return (
|
||||
<div>
|
||||
<Subtitle subtitle={t("permission.title")} />
|
||||
<table className="has-background-light table is-hoverable is-fullwidth">
|
||||
<table className="card-table table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
|
||||
@@ -35,7 +35,7 @@ class DeleteUser extends React.Component<Props> {
|
||||
};
|
||||
|
||||
userDeleted = () => {
|
||||
this.props.history.push("/users");
|
||||
this.props.history.push("/users/");
|
||||
};
|
||||
|
||||
deleteUser = () => {
|
||||
|
||||
@@ -275,6 +275,14 @@ ul.is-separated {
|
||||
.panel-block {
|
||||
display: block;
|
||||
border: none;
|
||||
|
||||
& .comment-wrapper:first-child div:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
& .diff-widget-content div {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
|
||||
1022
scm-ui/yarn.lock
1022
scm-ui/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -461,7 +461,6 @@
|
||||
<plugin>
|
||||
<groupId>sonia.scm.maven</groupId>
|
||||
<artifactId>smp-maven-plugin</artifactId>
|
||||
<version>1.0.0-alpha-2</version>
|
||||
<configuration>
|
||||
<artifactItems>
|
||||
<artifactItem>
|
||||
|
||||
@@ -52,7 +52,6 @@ import sonia.scm.plugin.ExtensionProcessor;
|
||||
import sonia.scm.plugin.PluginWrapper;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.schedule.Scheduler;
|
||||
import sonia.scm.upgrade.UpgradeManager;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
@@ -102,15 +101,6 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
|
||||
}
|
||||
|
||||
private void beforeInjectorCreation() {
|
||||
upgradeIfNecessary();
|
||||
}
|
||||
|
||||
private void upgradeIfNecessary() {
|
||||
if (!hasStartupErrors()) {
|
||||
UpgradeManager upgradeHandler = new UpgradeManager();
|
||||
|
||||
upgradeHandler.doUpgrade();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasStartupErrors() {
|
||||
@@ -132,9 +122,6 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
|
||||
List<Module> moduleList = Lists.newArrayList();
|
||||
|
||||
moduleList.add(new ResteasyModule());
|
||||
moduleList.add(new ScmInitializerModule());
|
||||
moduleList.add(new ScmEventBusModule());
|
||||
moduleList.add(new EagerSingletonModule());
|
||||
moduleList.add(ShiroWebModule.guiceFilterModule());
|
||||
moduleList.add(new WebElementModule(pluginLoader));
|
||||
moduleList.add(new ScmServletModule(context, pluginLoader, overrides));
|
||||
|
||||
@@ -53,8 +53,6 @@ import sonia.scm.group.GroupDisplayManager;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.group.GroupManagerProvider;
|
||||
import sonia.scm.group.xml.XmlGroupDAO;
|
||||
import sonia.scm.io.DefaultFileSystem;
|
||||
import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.net.SSLContextProvider;
|
||||
import sonia.scm.net.ahc.AdvancedHttpClient;
|
||||
import sonia.scm.net.ahc.ContentTransformer;
|
||||
@@ -63,7 +61,6 @@ import sonia.scm.net.ahc.JsonContentTransformer;
|
||||
import sonia.scm.net.ahc.XmlContentTransformer;
|
||||
import sonia.scm.plugin.DefaultPluginLoader;
|
||||
import sonia.scm.plugin.DefaultPluginManager;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
import sonia.scm.repository.DefaultRepositoryManager;
|
||||
import sonia.scm.repository.DefaultRepositoryProvider;
|
||||
@@ -87,23 +84,11 @@ import sonia.scm.schedule.QuartzScheduler;
|
||||
import sonia.scm.schedule.Scheduler;
|
||||
import sonia.scm.security.AccessTokenCookieIssuer;
|
||||
import sonia.scm.security.AuthorizationChangedEventProducer;
|
||||
import sonia.scm.security.CipherHandler;
|
||||
import sonia.scm.security.CipherUtil;
|
||||
import sonia.scm.security.ConfigurableLoginAttemptHandler;
|
||||
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
|
||||
import sonia.scm.security.DefaultKeyGenerator;
|
||||
import sonia.scm.security.DefaultSecuritySystem;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.security.LoginAttemptHandler;
|
||||
import sonia.scm.security.SecuritySystem;
|
||||
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;
|
||||
import sonia.scm.template.MustacheTemplateEngine;
|
||||
import sonia.scm.template.TemplateEngine;
|
||||
import sonia.scm.template.TemplateEngineFactory;
|
||||
|
||||
@@ -63,9 +63,7 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
||||
|
||||
builder.single(link("repositoryTypes", resourceLinks.repositoryTypeCollection().self()));
|
||||
builder.single(link("namespaceStrategies", resourceLinks.namespaceStrategies().self()));
|
||||
if (RepositoryRolePermissions.read().isPermitted()) {
|
||||
builder.single(link("repositoryRoles", resourceLinks.repositoryRoleCollection().self()));
|
||||
}
|
||||
} else {
|
||||
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
||||
}
|
||||
|
||||
@@ -7,12 +7,15 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class PermissionListDto extends HalRepresentation {
|
||||
|
||||
@NotNull
|
||||
private String[] permissions;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -25,7 +25,7 @@ public class RepositoryRoleCollectionToDtoMapper extends BasicCollectionToDtoMap
|
||||
}
|
||||
|
||||
Optional<String> createCreateLink() {
|
||||
return RepositoryRolePermissions.modify().isPermitted() ? of(resourceLinks.repositoryRoleCollection().create()): empty();
|
||||
return RepositoryRolePermissions.write().isPermitted() ? of(resourceLinks.repositoryRoleCollection().create()): empty();
|
||||
}
|
||||
|
||||
String createSelfLink() {
|
||||
|
||||
@@ -27,7 +27,7 @@ public abstract class RepositoryRoleToRepositoryRoleDtoMapper extends BaseMapper
|
||||
@ObjectFactory
|
||||
RepositoryRoleDto createDto(RepositoryRole repositoryRole) {
|
||||
Links.Builder linksBuilder = linkingTo().self(resourceLinks.repositoryRole().self(repositoryRole.getName()));
|
||||
if (!"system".equals(repositoryRole.getType()) && RepositoryRolePermissions.modify().isPermitted()) {
|
||||
if (!"system".equals(repositoryRole.getType()) && RepositoryRolePermissions.write().isPermitted()) {
|
||||
linksBuilder.single(link("delete", resourceLinks.repositoryRole().delete(repositoryRole.getName())));
|
||||
linksBuilder.single(link("update", resourceLinks.repositoryRole().update(repositoryRole.getName())));
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import sonia.scm.security.PermissionDescriptor;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
@@ -69,7 +70,7 @@ public class UserPermissionResource {
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
public Response overwritePermissions(@PathParam("id") String id, PermissionListDto newPermissions) {
|
||||
public Response overwritePermissions(@PathParam("id") String id, @Valid PermissionListDto newPermissions) {
|
||||
Collection<PermissionDescriptor> permissionDescriptors = Arrays.stream(newPermissions.getPermissions())
|
||||
.map(PermissionDescriptor::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@@ -39,14 +39,18 @@ import com.google.inject.Module;
|
||||
import com.google.inject.assistedinject.FactoryModuleBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.EagerSingletonModule;
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.ScmContextListener;
|
||||
import sonia.scm.ScmEventBusModule;
|
||||
import sonia.scm.ScmInitializerModule;
|
||||
import sonia.scm.Stage;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.plugin.DefaultPluginLoader;
|
||||
import sonia.scm.plugin.Plugin;
|
||||
import sonia.scm.plugin.PluginException;
|
||||
import sonia.scm.plugin.PluginLoadException;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.plugin.PluginWrapper;
|
||||
import sonia.scm.plugin.PluginsInternal;
|
||||
import sonia.scm.plugin.SmpArchive;
|
||||
@@ -134,6 +138,19 @@ public class BootstrapContextListener implements ServletContextListener {
|
||||
|
||||
File pluginDirectory = getPluginDirectory();
|
||||
|
||||
createContextListener(pluginDirectory);
|
||||
|
||||
contextListener.contextInitialized(sce);
|
||||
|
||||
// register for restart events
|
||||
if (!registered && (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)) {
|
||||
logger.info("register for restart events");
|
||||
ScmEventBus.getInstance().register(this);
|
||||
registered = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void createContextListener(File pluginDirectory) {
|
||||
try {
|
||||
if (!isCorePluginExtractionDisabled()) {
|
||||
extractCorePlugins(context, pluginDirectory);
|
||||
@@ -145,12 +162,9 @@ public class BootstrapContextListener implements ServletContextListener {
|
||||
|
||||
Set<PluginWrapper> plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath());
|
||||
|
||||
DefaultPluginLoader pluginLoader = new DefaultPluginLoader(context, cl, plugins);
|
||||
PluginLoader pluginLoader = new DefaultPluginLoader(context, cl, plugins);
|
||||
|
||||
Module scmContextListenerModule = new ScmContextListenerModule();
|
||||
BootstrapModule bootstrapModule = new BootstrapModule(pluginLoader);
|
||||
|
||||
Injector bootstrapInjector = Guice.createInjector(bootstrapModule, scmContextListenerModule);
|
||||
Injector bootstrapInjector = createBootstrapInjector(pluginLoader);
|
||||
|
||||
processUpdates(pluginLoader, bootstrapInjector);
|
||||
|
||||
@@ -158,19 +172,25 @@ public class BootstrapContextListener implements ServletContextListener {
|
||||
} catch (IOException ex) {
|
||||
throw new PluginLoadException("could not load plugins", ex);
|
||||
}
|
||||
|
||||
contextListener.contextInitialized(sce);
|
||||
|
||||
// register for restart events
|
||||
if (!registered
|
||||
&& (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)) {
|
||||
logger.info("register for restart events");
|
||||
ScmEventBus.getInstance().register(this);
|
||||
registered = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void processUpdates(DefaultPluginLoader pluginLoader, Injector bootstrapInjector) {
|
||||
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);
|
||||
@@ -390,7 +410,6 @@ public class BootstrapContextListener implements ServletContextListener {
|
||||
private static class ScmContextListenerModule extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
|
||||
install(new FactoryModuleBuilder().build(ScmContextListener.Factory.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import sonia.scm.SCMContext;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.io.DefaultFileSystem;
|
||||
import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.plugin.DefaultPluginLoader;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
||||
@@ -33,7 +32,7 @@ public class BootstrapModule extends AbstractModule {
|
||||
private final ClassOverrides overrides;
|
||||
private final PluginLoader pluginLoader;
|
||||
|
||||
BootstrapModule(DefaultPluginLoader pluginLoader) {
|
||||
BootstrapModule(PluginLoader pluginLoader) {
|
||||
this.overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader());
|
||||
this.pluginLoader = pluginLoader;
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
|
||||
|
||||
return managerDaoAdapter.create(
|
||||
repositoryRole,
|
||||
RepositoryRolePermissions::modify,
|
||||
RepositoryRolePermissions::write,
|
||||
newRepositoryRole -> fireEvent(HandlerEventType.BEFORE_CREATE, newRepositoryRole),
|
||||
newRepositoryRole -> fireEvent(HandlerEventType.CREATE, newRepositoryRole)
|
||||
);
|
||||
@@ -100,7 +100,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
|
||||
logger.info("delete repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType());
|
||||
managerDaoAdapter.delete(
|
||||
repositoryRole,
|
||||
RepositoryRolePermissions::modify,
|
||||
RepositoryRolePermissions::write,
|
||||
toDelete -> fireEvent(HandlerEventType.BEFORE_DELETE, toDelete),
|
||||
toDelete -> fireEvent(HandlerEventType.DELETE, toDelete)
|
||||
);
|
||||
@@ -116,7 +116,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
|
||||
logger.info("modify repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType());
|
||||
managerDaoAdapter.modify(
|
||||
repositoryRole,
|
||||
x -> RepositoryRolePermissions.modify(),
|
||||
x -> RepositoryRolePermissions.write(),
|
||||
notModified -> fireEvent(HandlerEventType.BEFORE_MODIFY, repositoryRole, notModified),
|
||||
notModified -> fireEvent(HandlerEventType.MODIFY, repositoryRole, notModified));
|
||||
}
|
||||
@@ -125,7 +125,6 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
|
||||
public void refresh(RepositoryRole repositoryRole) {
|
||||
logger.info("refresh repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType());
|
||||
|
||||
RepositoryRolePermissions.read().check();
|
||||
RepositoryRole fresh = repositoryRoleDAO.get(repositoryRole.getName());
|
||||
|
||||
if (fresh == null) {
|
||||
@@ -135,8 +134,6 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
|
||||
|
||||
@Override
|
||||
public RepositoryRole get(String id) {
|
||||
RepositoryRolePermissions.read().check();
|
||||
|
||||
return findSystemRole(id).orElse(findCustomRole(id));
|
||||
}
|
||||
|
||||
@@ -168,9 +165,6 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
|
||||
public List<RepositoryRole> getAll() {
|
||||
List<RepositoryRole> repositoryRoles = new ArrayList<>();
|
||||
|
||||
if (!RepositoryRolePermissions.read().isPermitted()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
for (RepositoryRole repositoryRole : repositoryPermissionProvider.availableRoles()) {
|
||||
repositoryRoles.add(repositoryRole.clone());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package sonia.scm.update.group;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.xml.XmlGroupDAO;
|
||||
import sonia.scm.migration.UpdateException;
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.store.ConfigurationEntryStore;
|
||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.StoreConstants;
|
||||
import sonia.scm.update.properties.V1Properties;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
import static sonia.scm.version.Version.parse;
|
||||
|
||||
@Extension
|
||||
public class XmlGroupV1UpdateStep implements UpdateStep {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XmlGroupV1UpdateStep.class);
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
private final XmlGroupDAO groupDAO;
|
||||
private final ConfigurationEntryStore<V1Properties> propertyStore;
|
||||
|
||||
@Inject
|
||||
public XmlGroupV1UpdateStep(
|
||||
SCMContextProvider contextProvider,
|
||||
XmlGroupDAO groupDAO,
|
||||
ConfigurationEntryStoreFactory configurationEntryStoreFactory
|
||||
) {
|
||||
this.contextProvider = contextProvider;
|
||||
this.groupDAO = groupDAO;
|
||||
this.propertyStore = configurationEntryStoreFactory
|
||||
.withType(V1Properties.class)
|
||||
.withName("group-properties-v1")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doUpdate() throws JAXBException {
|
||||
Optional<Path> v1GroupsFile = determineV1File();
|
||||
if (!v1GroupsFile.isPresent()) {
|
||||
LOG.info("no v1 file for groups found");
|
||||
return;
|
||||
}
|
||||
XmlGroupV1UpdateStep.V1GroupDatabase v1Database = readV1Database(v1GroupsFile.get());
|
||||
if (v1Database.groupList != null && v1Database.groupList.groups != null) {
|
||||
v1Database.groupList.groups.forEach(this::update);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getTargetVersion() {
|
||||
return parse("2.0.0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffectedDataType() {
|
||||
return "sonia.scm.group.xml";
|
||||
}
|
||||
|
||||
private void update(V1Group v1Group) {
|
||||
LOG.debug("updating group {}", v1Group.name);
|
||||
Group group = new Group(
|
||||
v1Group.type,
|
||||
v1Group.name,
|
||||
v1Group.members);
|
||||
group.setDescription(v1Group.description);
|
||||
group.setCreationDate(v1Group.creationDate);
|
||||
group.setLastModified(v1Group.lastModified);
|
||||
groupDAO.add(group);
|
||||
|
||||
propertyStore.put(v1Group.name, v1Group.properties);
|
||||
}
|
||||
|
||||
private XmlGroupV1UpdateStep.V1GroupDatabase readV1Database(Path v1GroupsFile) throws JAXBException {
|
||||
JAXBContext jaxbContext = JAXBContext.newInstance(XmlGroupV1UpdateStep.V1GroupDatabase.class);
|
||||
return (XmlGroupV1UpdateStep.V1GroupDatabase) jaxbContext.createUnmarshaller().unmarshal(v1GroupsFile.toFile());
|
||||
}
|
||||
|
||||
private Optional<Path> determineV1File() {
|
||||
Path existingGroupsFile = resolveConfigFile("groups");
|
||||
Path groupsV1File = resolveConfigFile("groupsV1");
|
||||
if (existingGroupsFile.toFile().exists()) {
|
||||
try {
|
||||
Files.move(existingGroupsFile, groupsV1File);
|
||||
} catch (IOException e) {
|
||||
throw new UpdateException("could not move old groups file to " + groupsV1File.toAbsolutePath());
|
||||
}
|
||||
LOG.info("moved old groups file to {}", groupsV1File.toAbsolutePath());
|
||||
return of(groupsV1File);
|
||||
}
|
||||
return empty();
|
||||
}
|
||||
|
||||
private Path resolveConfigFile(String name) {
|
||||
return contextProvider
|
||||
.resolve(
|
||||
Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve(name + StoreConstants.FILE_EXTENSION)
|
||||
);
|
||||
}
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "group")
|
||||
private static class V1Group {
|
||||
private V1Properties properties;
|
||||
private long creationDate;
|
||||
private String description;
|
||||
private Long lastModified;
|
||||
private String name;
|
||||
private String type;
|
||||
@XmlElement(name = "members")
|
||||
private List<String> members;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "V1Group{" +
|
||||
"properties=" + properties +
|
||||
", creationDate=" + creationDate + '\'' +
|
||||
", description=" + description + '\'' +
|
||||
", lastModified=" + lastModified + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", type='" + type + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
private static class GroupList {
|
||||
@XmlElement(name = "group")
|
||||
private List<V1Group> groups;
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "group-db")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class V1GroupDatabase {
|
||||
private long creationTime;
|
||||
private Long lastModified;
|
||||
@XmlElement(name = "groups")
|
||||
private XmlGroupV1UpdateStep.GroupList groupList;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package sonia.scm.update.properties;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.util.List;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "properties")
|
||||
public class V1Properties {
|
||||
@XmlElement(name = "item")
|
||||
private List<V1Property> properties;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package sonia.scm.update.properties;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class V1Property {
|
||||
private String key;
|
||||
private String value;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.migration.UpdateException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
abstract class BaseMigrationStrategy implements MigrationStrategy.Instance {
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
|
||||
BaseMigrationStrategy(SCMContextProvider contextProvider) {
|
||||
this.contextProvider = contextProvider;
|
||||
}
|
||||
|
||||
Path getSourceDataPath(String name, String type) {
|
||||
return Arrays.stream(name.split("/"))
|
||||
.reduce(getTypeDependentPath(type), (path, namePart) -> path.resolve(namePart), (p1, p2) -> p1);
|
||||
}
|
||||
|
||||
Path getTypeDependentPath(String type) {
|
||||
return contextProvider.getBaseDirectory().toPath().resolve("repositories").resolve(type);
|
||||
}
|
||||
|
||||
Stream<Path> listSourceDirectory(Path sourceDirectory) {
|
||||
try {
|
||||
return Files.list(sourceDirectory);
|
||||
} catch (IOException e) {
|
||||
throw new UpdateException("could not read original directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
void createDataDirectory(Path target) {
|
||||
try {
|
||||
Files.createDirectories(target);
|
||||
} catch (IOException e) {
|
||||
throw new UpdateException("could not create data directory " + target, e);
|
||||
}
|
||||
}
|
||||
|
||||
void moveFile(Path sourceFile, Path targetFile) {
|
||||
try {
|
||||
Files.move(sourceFile, targetFile);
|
||||
} catch (IOException e) {
|
||||
throw new UpdateException("could not move data file from " + sourceFile + " to " + targetFile, e);
|
||||
}
|
||||
}
|
||||
|
||||
void copyFile(Path sourceFile, Path targetFile) {
|
||||
try {
|
||||
Files.copy(sourceFile, targetFile);
|
||||
} catch (IOException e) {
|
||||
throw new UpdateException("could not copy original file from " + sourceFile + " to " + targetFile, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.RepositoryDirectoryHandler;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
class CopyMigrationStrategy extends BaseMigrationStrategy {
|
||||
|
||||
private final RepositoryLocationResolver locationResolver;
|
||||
|
||||
@Inject
|
||||
public CopyMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) {
|
||||
super(contextProvider);
|
||||
this.locationResolver = locationResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path migrate(String id, String name, String type) {
|
||||
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
|
||||
Path targetDataPath = repositoryBasePath
|
||||
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||
Path sourceDataPath = getSourceDataPath(name, type);
|
||||
copyData(sourceDataPath, targetDataPath);
|
||||
return repositoryBasePath;
|
||||
}
|
||||
|
||||
private void copyData(Path sourceDirectory, Path targetDirectory) {
|
||||
createDataDirectory(targetDirectory);
|
||||
listSourceDirectory(sourceDirectory).forEach(
|
||||
sourceFile -> {
|
||||
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
|
||||
if (Files.isDirectory(sourceFile)) {
|
||||
copyData(sourceFile, targetFile);
|
||||
} else {
|
||||
copyFile(sourceFile, targetFile);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.RepositoryDirectoryHandler;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
class InlineMigrationStrategy extends BaseMigrationStrategy {
|
||||
|
||||
@Inject
|
||||
public InlineMigrationStrategy(SCMContextProvider contextProvider) {
|
||||
super(contextProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path migrate(String id, String name, String type) {
|
||||
Path repositoryBasePath = getSourceDataPath(name, type);
|
||||
Path targetDataPath = repositoryBasePath
|
||||
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||
moveData(repositoryBasePath, targetDataPath);
|
||||
return repositoryBasePath;
|
||||
}
|
||||
|
||||
private void moveData(Path sourceDirectory, Path targetDirectory) {
|
||||
createDataDirectory(targetDirectory);
|
||||
listSourceDirectory(sourceDirectory)
|
||||
.filter(sourceFile -> !targetDirectory.equals(sourceFile))
|
||||
.forEach(
|
||||
sourceFile -> {
|
||||
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
|
||||
if (Files.isDirectory(sourceFile)) {
|
||||
moveData(sourceFile, targetFile);
|
||||
} else {
|
||||
moveFile(sourceFile, targetFile);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package sonia.scm.repository.update;
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -0,0 +1,26 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import com.google.inject.Injector;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
enum MigrationStrategy {
|
||||
|
||||
COPY(CopyMigrationStrategy.class),
|
||||
MOVE(MoveMigrationStrategy.class),
|
||||
INLINE(InlineMigrationStrategy.class);
|
||||
|
||||
private Class<? extends Instance> implementationClass;
|
||||
|
||||
MigrationStrategy(Class<? extends Instance> implementationClass) {
|
||||
this.implementationClass = implementationClass;
|
||||
}
|
||||
|
||||
Instance from(Injector injector) {
|
||||
return injector.getInstance(implementationClass);
|
||||
}
|
||||
|
||||
interface Instance {
|
||||
Path migrate(String id, String name, String type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import sonia.scm.store.ConfigurationStore;
|
||||
import sonia.scm.store.ConfigurationStoreFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MigrationStrategyDao {
|
||||
|
||||
private final RepositoryMigrationPlan plan;
|
||||
private final ConfigurationStore<RepositoryMigrationPlan> store;
|
||||
|
||||
@Inject
|
||||
public MigrationStrategyDao(ConfigurationStoreFactory storeFactory) {
|
||||
store = storeFactory.withType(RepositoryMigrationPlan.class).withName("migration-plan").build();
|
||||
this.plan = store.getOptional().orElse(new RepositoryMigrationPlan());
|
||||
}
|
||||
|
||||
public Optional<MigrationStrategy> get(String id) {
|
||||
return plan.get(id);
|
||||
}
|
||||
|
||||
public void set(String repositoryId, MigrationStrategy strategy) {
|
||||
plan.set(repositoryId, strategy);
|
||||
store.set(plan);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.RepositoryDirectoryHandler;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
class MoveMigrationStrategy extends BaseMigrationStrategy {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MoveMigrationStrategy.class);
|
||||
|
||||
private final RepositoryLocationResolver locationResolver;
|
||||
|
||||
@Inject
|
||||
public MoveMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) {
|
||||
super(contextProvider);
|
||||
this.locationResolver = locationResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path migrate(String id, String name, String type) {
|
||||
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
|
||||
Path targetDataPath = repositoryBasePath
|
||||
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||
Path sourceDataPath = getSourceDataPath(name, type);
|
||||
moveData(sourceDataPath, targetDataPath);
|
||||
deleteOldDataDir(getTypeDependentPath(type), name);
|
||||
return repositoryBasePath;
|
||||
}
|
||||
|
||||
private void deleteOldDataDir(Path rootPath, String name) {
|
||||
delete(rootPath, asList(name.split("/")));
|
||||
}
|
||||
|
||||
private void delete(Path rootPath, List<String> directories) {
|
||||
if (directories.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Path directory = rootPath.resolve(directories.get(0));
|
||||
delete(directory, directories.subList(1, directories.size()));
|
||||
try {
|
||||
Files.deleteIfExists(directory);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("could not delete source repository directory {}", directory);
|
||||
}
|
||||
}
|
||||
|
||||
private void moveData(Path sourceDirectory, Path targetDirectory) {
|
||||
createDataDirectory(targetDirectory);
|
||||
listSourceDirectory(sourceDirectory).forEach(
|
||||
sourceFile -> {
|
||||
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
|
||||
if (Files.isDirectory(sourceFile)) {
|
||||
moveData(sourceFile, targetFile);
|
||||
} else {
|
||||
moveFile(sourceFile, targetFile);
|
||||
}
|
||||
}
|
||||
);
|
||||
try {
|
||||
Files.delete(sourceDirectory);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("could not delete source repository directory {}", sourceDirectory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "repository-migration")
|
||||
class RepositoryMigrationPlan {
|
||||
|
||||
private List<RepositoryEntry> entries;
|
||||
|
||||
RepositoryMigrationPlan() {
|
||||
this(new RepositoryEntry[0]);
|
||||
}
|
||||
|
||||
RepositoryMigrationPlan(RepositoryEntry... entries) {
|
||||
this.entries = new ArrayList<>(asList(entries));
|
||||
}
|
||||
|
||||
Optional<MigrationStrategy> get(String repositoryId) {
|
||||
return findEntry(repositoryId)
|
||||
.map(RepositoryEntry::getDataMigrationStrategy);
|
||||
}
|
||||
|
||||
public void set(String repositoryId, MigrationStrategy strategy) {
|
||||
Optional<RepositoryEntry> entry = findEntry(repositoryId);
|
||||
if (entry.isPresent()) {
|
||||
entry.get().setStrategy(strategy);
|
||||
} else {
|
||||
entries.add(new RepositoryEntry(repositoryId, strategy));
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<RepositoryEntry> findEntry(String repositoryId) {
|
||||
return entries.stream()
|
||||
.filter(repositoryEntry -> repositoryId.equals(repositoryEntry.repositoryId))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "entries")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
static class RepositoryEntry {
|
||||
|
||||
private String repositoryId;
|
||||
private MigrationStrategy dataMigrationStrategy;
|
||||
|
||||
RepositoryEntry() {
|
||||
}
|
||||
|
||||
RepositoryEntry(String repositoryId, MigrationStrategy dataMigrationStrategy) {
|
||||
this.repositoryId = repositoryId;
|
||||
this.dataMigrationStrategy = dataMigrationStrategy;
|
||||
}
|
||||
|
||||
public MigrationStrategy getDataMigrationStrategy() {
|
||||
return dataMigrationStrategy;
|
||||
}
|
||||
|
||||
private void setStrategy(MigrationStrategy strategy) {
|
||||
this.dataMigrationStrategy = strategy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package sonia.scm.repository.update;
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
public class RepositoryUpdates {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
||||
import sonia.scm.repository.xml.XmlRepositoryDAO;
|
||||
import sonia.scm.store.StoreConstants;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static sonia.scm.version.Version.parse;
|
||||
|
||||
/**
|
||||
* Moves an existing <code>repositories.xml</code> file to <code>repository-paths.xml</code>.
|
||||
* Note that this has to run <em>after</em> an old v1 repository database has been migrated to v2
|
||||
* (see {@link XmlRepositoryV1UpdateStep}).
|
||||
*/
|
||||
@Extension
|
||||
public class XmlRepositoryFileNameUpdateStep implements UpdateStep {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XmlRepositoryFileNameUpdateStep.class);
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
private final XmlRepositoryDAO repositoryDAO;
|
||||
|
||||
@Inject
|
||||
public XmlRepositoryFileNameUpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO repositoryDAO) {
|
||||
this.contextProvider = contextProvider;
|
||||
this.repositoryDAO = repositoryDAO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doUpdate() throws IOException {
|
||||
Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
|
||||
Path oldRepositoriesFile = configDir.resolve("repositories.xml");
|
||||
Path newRepositoryPathsFile = configDir.resolve(PathBasedRepositoryLocationResolver.STORE_NAME + StoreConstants.FILE_EXTENSION);
|
||||
if (Files.exists(oldRepositoriesFile)) {
|
||||
LOG.info("moving old repositories database files to repository-paths file");
|
||||
Files.move(oldRepositoriesFile, newRepositoryPathsFile);
|
||||
repositoryDAO.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getTargetVersion() {
|
||||
return parse("2.0.1");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffectedDataType() {
|
||||
return "sonia.scm.repository.xml";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import com.google.inject.Injector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.migration.UpdateException;
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.xml.XmlRepositoryDAO;
|
||||
import sonia.scm.store.ConfigurationEntryStore;
|
||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.StoreConstants;
|
||||
import sonia.scm.update.properties.V1Properties;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
import static sonia.scm.version.Version.parse;
|
||||
|
||||
/**
|
||||
* Migrates SCM-Manager v1 repository data structure to SCM-Manager v2 data structure.
|
||||
* That is:
|
||||
* <ul>
|
||||
* <li>The old <code>repositories.xml</code> file is read</li>
|
||||
* <li>For each repository in this database,
|
||||
* <ul>
|
||||
* <li>a new entry in the new <code>repository-paths.xml</code> database is written,</li>
|
||||
* <li>the data directory is moved or copied to a SCM v2 consistent directory. How this is done
|
||||
* can be specified by a strategy (@see {@link MigrationStrategy}), that has to be set in
|
||||
* a database file named <code>migration-plan.xml</code></li> (to create this file, use {@link MigrationStrategyDao}),
|
||||
* and
|
||||
* <li>the new <code>metadata.xml</code> file is created.</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
@Extension
|
||||
public class XmlRepositoryV1UpdateStep implements UpdateStep {
|
||||
|
||||
private static Logger LOG = LoggerFactory.getLogger(XmlRepositoryV1UpdateStep.class);
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
private final XmlRepositoryDAO repositoryDao;
|
||||
private final MigrationStrategyDao migrationStrategyDao;
|
||||
private final Injector injector;
|
||||
private final ConfigurationEntryStore<V1Properties> propertyStore;
|
||||
|
||||
@Inject
|
||||
public XmlRepositoryV1UpdateStep(
|
||||
SCMContextProvider contextProvider,
|
||||
XmlRepositoryDAO repositoryDao,
|
||||
MigrationStrategyDao migrationStrategyDao,
|
||||
Injector injector,
|
||||
ConfigurationEntryStoreFactory configurationEntryStoreFactory
|
||||
) {
|
||||
this.contextProvider = contextProvider;
|
||||
this.repositoryDao = repositoryDao;
|
||||
this.migrationStrategyDao = migrationStrategyDao;
|
||||
this.injector = injector;
|
||||
this.propertyStore = configurationEntryStoreFactory
|
||||
.withType(V1Properties.class)
|
||||
.withName("repository-properties-v1")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getTargetVersion() {
|
||||
return parse("2.0.0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffectedDataType() {
|
||||
return "sonia.scm.repository.xml";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doUpdate() throws JAXBException {
|
||||
if (!resolveV1File().exists()) {
|
||||
LOG.info("no v1 repositories database file found");
|
||||
return;
|
||||
}
|
||||
JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class);
|
||||
readV1Database(jaxbContext).ifPresent(
|
||||
v1Database -> {
|
||||
v1Database.repositoryList.repositories.forEach(this::readMigrationStrategy);
|
||||
v1Database.repositoryList.repositories.forEach(this::update);
|
||||
backupOldRepositoriesFile();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void backupOldRepositoriesFile() {
|
||||
Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
|
||||
Path oldRepositoriesFile = configDir.resolve("repositories.xml");
|
||||
Path backupFile = configDir.resolve("repositories.xml.v1.backup");
|
||||
LOG.info("moving old repositories database files to backup file {}", backupFile);
|
||||
try {
|
||||
Files.move(oldRepositoriesFile, backupFile);
|
||||
} catch (IOException e) {
|
||||
throw new UpdateException("could not backup old repository database file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void update(V1Repository v1Repository) {
|
||||
Path destination = handleDataDirectory(v1Repository);
|
||||
Repository repository = new Repository(
|
||||
v1Repository.id,
|
||||
v1Repository.type,
|
||||
getNamespace(v1Repository),
|
||||
getName(v1Repository),
|
||||
v1Repository.contact,
|
||||
v1Repository.description,
|
||||
createPermissions(v1Repository));
|
||||
LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.name, destination);
|
||||
repositoryDao.add(repository, destination);
|
||||
propertyStore.put(v1Repository.id, v1Repository.properties);
|
||||
}
|
||||
|
||||
private Path handleDataDirectory(V1Repository v1Repository) {
|
||||
MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository);
|
||||
return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type);
|
||||
}
|
||||
|
||||
private MigrationStrategy readMigrationStrategy(V1Repository v1Repository) {
|
||||
return migrationStrategyDao.get(v1Repository.id)
|
||||
.orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name));
|
||||
}
|
||||
|
||||
private RepositoryPermission[] createPermissions(V1Repository v1Repository) {
|
||||
if (v1Repository.permissions == null) {
|
||||
return new RepositoryPermission[0];
|
||||
}
|
||||
return v1Repository.permissions
|
||||
.stream()
|
||||
.map(this::createPermission)
|
||||
.toArray(RepositoryPermission[]::new);
|
||||
}
|
||||
|
||||
private RepositoryPermission createPermission(V1Permission v1Permission) {
|
||||
LOG.info("creating permission {} for {}", v1Permission.type, v1Permission.name);
|
||||
return new RepositoryPermission(v1Permission.name, v1Permission.type, v1Permission.groupPermission);
|
||||
}
|
||||
|
||||
private String getNamespace(V1Repository v1Repository) {
|
||||
String[] nameParts = getNameParts(v1Repository.name);
|
||||
return nameParts.length > 1 ? nameParts[0] : v1Repository.type;
|
||||
}
|
||||
|
||||
private String getName(V1Repository v1Repository) {
|
||||
String[] nameParts = getNameParts(v1Repository.name);
|
||||
return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts);
|
||||
}
|
||||
|
||||
private String concatPathElements(String[] nameParts) {
|
||||
return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_"));
|
||||
}
|
||||
|
||||
private String[] getNameParts(String v1Name) {
|
||||
return v1Name.split("/");
|
||||
}
|
||||
|
||||
private Optional<V1RepositoryDatabase> readV1Database(JAXBContext jaxbContext) throws JAXBException {
|
||||
Object unmarshal = jaxbContext.createUnmarshaller().unmarshal(resolveV1File());
|
||||
if (unmarshal instanceof V1RepositoryDatabase) {
|
||||
return of((V1RepositoryDatabase) unmarshal);
|
||||
} else {
|
||||
return empty();
|
||||
}
|
||||
}
|
||||
|
||||
private File resolveV1File() {
|
||||
return contextProvider
|
||||
.resolve(
|
||||
Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve("repositories" + StoreConstants.FILE_EXTENSION)
|
||||
).toFile();
|
||||
}
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "permissions")
|
||||
private static class V1Permission {
|
||||
private boolean groupPermission;
|
||||
private String name;
|
||||
private String type;
|
||||
}
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "repositories")
|
||||
private static class V1Repository {
|
||||
private String contact;
|
||||
private long creationDate;
|
||||
private Long lastModified;
|
||||
private String description;
|
||||
private String id;
|
||||
private String name;
|
||||
private boolean isPublic;
|
||||
private boolean archived;
|
||||
private String type;
|
||||
private List<V1Permission> permissions;
|
||||
private V1Properties properties;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "V1Repository{" +
|
||||
", contact='" + contact + '\'' +
|
||||
", creationDate=" + creationDate +
|
||||
", lastModified=" + lastModified +
|
||||
", description='" + description + '\'' +
|
||||
", id='" + id + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", isPublic=" + isPublic +
|
||||
", archived=" + archived +
|
||||
", type='" + type + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
private static class RepositoryList {
|
||||
@XmlElement(name = "repository")
|
||||
private List<V1Repository> repositories;
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "repository-db")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class V1RepositoryDatabase {
|
||||
private long creationTime;
|
||||
private Long lastModified;
|
||||
@XmlElement(name = "repositories")
|
||||
private RepositoryList repositoryList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package sonia.scm.update.security;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.security.AssignedPermission;
|
||||
import sonia.scm.store.ConfigurationEntryStore;
|
||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.StoreConstants;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static sonia.scm.version.Version.parse;
|
||||
|
||||
@Extension
|
||||
public class XmlSecurityV1UpdateStep implements UpdateStep {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XmlSecurityV1UpdateStep.class);
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
private final ConfigurationEntryStoreFactory configurationEntryStoreFactory;
|
||||
|
||||
@Inject
|
||||
public XmlSecurityV1UpdateStep(SCMContextProvider contextProvider, ConfigurationEntryStoreFactory configurationEntryStoreFactory) {
|
||||
this.contextProvider = contextProvider;
|
||||
this.configurationEntryStoreFactory = configurationEntryStoreFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doUpdate() throws JAXBException {
|
||||
ConfigurationEntryStore<AssignedPermission> securityStore = createSecurityStore();
|
||||
|
||||
forAllAdmins(user -> createSecurityEntry(user, false, securityStore),
|
||||
group -> createSecurityEntry(group, true, securityStore));
|
||||
}
|
||||
|
||||
private void forAllAdmins(Consumer<String> userConsumer, Consumer<String> groupConsumer) throws JAXBException {
|
||||
Path configDirectory = determineConfigDirectory();
|
||||
Path existingConfigFile = configDirectory.resolve("config" + StoreConstants.FILE_EXTENSION);
|
||||
if (existingConfigFile.toFile().exists()) {
|
||||
forAllAdmins(existingConfigFile, userConsumer, groupConsumer);
|
||||
}
|
||||
}
|
||||
|
||||
private void forAllAdmins(
|
||||
Path existingConfigFile, Consumer<String> userConsumer, Consumer<String> groupConsumer
|
||||
) throws JAXBException {
|
||||
JAXBContext jaxbContext = JAXBContext.newInstance(XmlSecurityV1UpdateStep.V1Configuration.class);
|
||||
V1Configuration v1Configuration = (V1Configuration) jaxbContext.createUnmarshaller().unmarshal(existingConfigFile.toFile());
|
||||
|
||||
ofNullable(v1Configuration.adminUsers).ifPresent(users -> forAll(users, userConsumer));
|
||||
ofNullable(v1Configuration.adminGroups).ifPresent(groups -> forAll(groups, groupConsumer));
|
||||
}
|
||||
|
||||
private void forAll(String entries, Consumer<String> consumer) {
|
||||
Arrays.stream(entries.split(",")).forEach(consumer);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Version getTargetVersion() {
|
||||
return parse("2.0.0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffectedDataType() {
|
||||
return "sonia.scm.security.xml";
|
||||
}
|
||||
|
||||
private void createSecurityEntry(String name, boolean group, ConfigurationEntryStore<AssignedPermission> securityStore) {
|
||||
LOG.debug("setting admin permissions for {} {}", group? "group": "user", name);
|
||||
securityStore.put(new AssignedPermission(name, group, "*"));
|
||||
}
|
||||
|
||||
private ConfigurationEntryStore<AssignedPermission> createSecurityStore() {
|
||||
return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build();
|
||||
}
|
||||
|
||||
private Path determineConfigDirectory() {
|
||||
return new File(contextProvider.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME).toPath();
|
||||
}
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "scm-config")
|
||||
private static class V1Configuration {
|
||||
@XmlElement(name = "admin-users")
|
||||
private String adminUsers;
|
||||
@XmlElement(name = "admin-groups")
|
||||
private String adminGroups;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package sonia.scm.update.user;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.migration.UpdateException;
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.security.AssignedPermission;
|
||||
import sonia.scm.store.ConfigurationEntryStore;
|
||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.StoreConstants;
|
||||
import sonia.scm.update.properties.V1Properties;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.xml.XmlUserDAO;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
import static sonia.scm.version.Version.parse;
|
||||
|
||||
@Extension
|
||||
public class XmlUserV1UpdateStep implements UpdateStep {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XmlUserV1UpdateStep.class);
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
private final XmlUserDAO userDAO;
|
||||
private final ConfigurationEntryStoreFactory configurationEntryStoreFactory;
|
||||
private final ConfigurationEntryStore<V1Properties> propertyStore;
|
||||
|
||||
@Inject
|
||||
public XmlUserV1UpdateStep(SCMContextProvider contextProvider, XmlUserDAO userDAO, ConfigurationEntryStoreFactory configurationEntryStoreFactory) {
|
||||
this.contextProvider = contextProvider;
|
||||
this.userDAO = userDAO;
|
||||
this.configurationEntryStoreFactory = configurationEntryStoreFactory;
|
||||
this.propertyStore = configurationEntryStoreFactory
|
||||
.withType(V1Properties.class)
|
||||
.withName("user-properties-v1")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doUpdate() throws JAXBException {
|
||||
Optional<Path> v1UsersFile = determineV1File();
|
||||
if (!v1UsersFile.isPresent()) {
|
||||
LOG.info("no v1 file for users found");
|
||||
return;
|
||||
}
|
||||
XmlUserV1UpdateStep.V1UserDatabase v1Database = readV1Database(v1UsersFile.get());
|
||||
ConfigurationEntryStore<AssignedPermission> securityStore = createSecurityStore();
|
||||
v1Database.userList.users.forEach(user -> update(user, securityStore));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getTargetVersion() {
|
||||
return parse("2.0.0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffectedDataType() {
|
||||
return "sonia.scm.user.xml";
|
||||
}
|
||||
|
||||
private void update(V1User v1User, ConfigurationEntryStore<AssignedPermission> securityStore) {
|
||||
LOG.debug("updating user {}", v1User.name);
|
||||
User user = new User(
|
||||
v1User.name,
|
||||
v1User.displayName,
|
||||
v1User.mail,
|
||||
v1User.password,
|
||||
v1User.type,
|
||||
v1User.active);
|
||||
user.setCreationDate(v1User.creationDate);
|
||||
user.setLastModified(v1User.lastModified);
|
||||
userDAO.add(user);
|
||||
|
||||
if (v1User.admin) {
|
||||
LOG.debug("setting admin permissions for user {}", v1User.name);
|
||||
securityStore.put(new AssignedPermission(v1User.name, "*"));
|
||||
}
|
||||
|
||||
propertyStore.put(v1User.name, v1User.properties);
|
||||
}
|
||||
|
||||
private XmlUserV1UpdateStep.V1UserDatabase readV1Database(Path v1UsersFile) throws JAXBException {
|
||||
JAXBContext jaxbContext = JAXBContext.newInstance(XmlUserV1UpdateStep.V1UserDatabase.class);
|
||||
return (XmlUserV1UpdateStep.V1UserDatabase) jaxbContext.createUnmarshaller().unmarshal(v1UsersFile.toFile());
|
||||
}
|
||||
|
||||
private ConfigurationEntryStore<AssignedPermission> createSecurityStore() {
|
||||
return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build();
|
||||
}
|
||||
|
||||
private Optional<Path> determineV1File() {
|
||||
Path existingUsersFile = resolveConfigFile("users");
|
||||
Path usersV1File = resolveConfigFile("usersV1");
|
||||
if (existingUsersFile.toFile().exists()) {
|
||||
try {
|
||||
Files.move(existingUsersFile, usersV1File);
|
||||
} catch (IOException e) {
|
||||
throw new UpdateException("could not move old users file to " + usersV1File.toAbsolutePath());
|
||||
}
|
||||
LOG.info("moved old users file to {}", usersV1File.toAbsolutePath());
|
||||
return of(usersV1File);
|
||||
}
|
||||
return empty();
|
||||
}
|
||||
|
||||
private Path resolveConfigFile(String name) {
|
||||
return contextProvider
|
||||
.resolve(
|
||||
Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve(name + StoreConstants.FILE_EXTENSION)
|
||||
);
|
||||
}
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "user")
|
||||
private static class V1User {
|
||||
private V1Properties properties;
|
||||
private boolean admin;
|
||||
private long creationDate;
|
||||
private String displayName;
|
||||
private Long lastModified;
|
||||
private String mail;
|
||||
private String name;
|
||||
private String password;
|
||||
private String type;
|
||||
private boolean active;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "V1User{" +
|
||||
"properties=" + properties +
|
||||
", admin='" + admin + '\'' +
|
||||
", creationDate=" + creationDate + '\'' +
|
||||
", displayName=" + displayName + '\'' +
|
||||
", lastModified=" + lastModified + '\'' +
|
||||
", mail='" + mail + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", type='" + type + '\'' +
|
||||
", active='" + active + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
private static class UserList {
|
||||
@XmlElement(name = "user")
|
||||
private List<V1User> users;
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "user-db")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class V1UserDatabase {
|
||||
private long creationTime;
|
||||
private Long lastModified;
|
||||
@XmlElement(name = "users")
|
||||
private XmlUserV1UpdateStep.UserList userList;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package sonia.scm.upgrade;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public final class ClientDateFormatConverter
|
||||
{
|
||||
|
||||
/** Field description */
|
||||
private static final String SINGLECHAR_REGEX = "(^|[^%s])[%s]($|[^%s])";
|
||||
|
||||
/**
|
||||
* the logger for DateFormatConverter
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(ClientDateFormatConverter.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*/
|
||||
private ClientDateFormatConverter() {}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Documentations:
|
||||
* - Extjs: http://trac.geoext.org/browser/ext/3.4.0/docs/source/Date.html
|
||||
* - Moments: http://momentjs.com/docs/#/displaying/format
|
||||
*
|
||||
*
|
||||
* @param value
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String extjsToMoments(String value)
|
||||
{
|
||||
logger.trace(
|
||||
"try to convert extjs date format \"{}\" to moments date format", value);
|
||||
|
||||
String result = replaceDateFormatChars(value, "d", "DD");
|
||||
|
||||
result = replaceDateFormatChars(result, "D", "ddd");
|
||||
result = replaceDateFormatChars(result, "j", "D");
|
||||
result = replaceDateFormatChars(result, "l", "dddd");
|
||||
|
||||
// no replacement found for 1-7, only 0-6 found
|
||||
result = replaceDateFormatChars(result, "N", "d");
|
||||
result = replaceDateFormatChars(result, "w", "d");
|
||||
result = replaceDateFormatChars(result, "z", "DDDD");
|
||||
result = replaceDateFormatChars(result, "W", "ww");
|
||||
result = replaceDateFormatChars(result, "M", "MMM");
|
||||
result = replaceDateFormatChars(result, "F", "MMMM");
|
||||
result = replaceDateFormatChars(result, "m", "MM");
|
||||
result = replaceDateFormatChars(result, "n", "M");
|
||||
result = replaceDateFormatChars(result, "Y", "YYYY");
|
||||
result = replaceDateFormatChars(result, "o", "YYYY");
|
||||
result = replaceDateFormatChars(result, "y", "YY");
|
||||
result = replaceDateFormatChars(result, "H", "HH");
|
||||
result = replaceDateFormatChars(result, "h", "hh");
|
||||
result = replaceDateFormatChars(result, "g", "h");
|
||||
result = replaceDateFormatChars(result, "G", "H");
|
||||
result = replaceDateFormatChars(result, "i", "mm");
|
||||
result = replaceDateFormatChars(result, "s", "ss");
|
||||
result = replaceDateFormatChars(result, "O", "ZZ");
|
||||
result = replaceDateFormatChars(result, "P", "Z");
|
||||
result = replaceDateFormatChars(result, "T", "z");
|
||||
|
||||
logger.debug(
|
||||
"converted extjs date format \"{}\" to moments date format \"{}\"",
|
||||
value, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param value
|
||||
* @param c
|
||||
* @param replacement
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private static String replaceDateFormatChars(String value, String c,
|
||||
String replacement)
|
||||
{
|
||||
Pattern p = Pattern.compile(String.format(SINGLECHAR_REGEX, c, c, c));
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
Matcher m = p.matcher(value);
|
||||
|
||||
while (m.find())
|
||||
{
|
||||
m.appendReplacement(buffer, "$1" + replacement + "$2");
|
||||
}
|
||||
|
||||
m.appendTail(buffer);
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.upgrade;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.io.Files;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class ClientDateFormatUpgradeHandler extends XmlUpgradeHandler
|
||||
{
|
||||
|
||||
/**
|
||||
* the logger for ClientDateFormatUpgradeHandler
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(ClientDateFormatUpgradeHandler.class);
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param homeDirectory
|
||||
* @param configDirectory
|
||||
* @param oldVersion
|
||||
* @param newVersion
|
||||
*/
|
||||
@Override
|
||||
public void doUpgrade(File homeDirectory, File configDirectory,
|
||||
Version oldVersion, Version newVersion)
|
||||
{
|
||||
if (oldVersion.isOlder("1.23"))
|
||||
{
|
||||
if (logger.isInfoEnabled())
|
||||
{
|
||||
logger.info("data format is older than 1.23, upgrade to version {}",
|
||||
SCMContext.getContext().getVersion());
|
||||
}
|
||||
|
||||
updateClientDateFormat(homeDirectory, configDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param document
|
||||
*/
|
||||
private void fixClientDateFormat(Document document)
|
||||
{
|
||||
NodeList nodes = document.getElementsByTagName("dateFormat");
|
||||
|
||||
if (nodes != null)
|
||||
{
|
||||
for (int i = 0; i < nodes.getLength(); i++)
|
||||
{
|
||||
Node node = nodes.item(i);
|
||||
String value = node.getTextContent();
|
||||
|
||||
value = ClientDateFormatConverter.extjsToMoments(value);
|
||||
node.setTextContent(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param baseDirectory
|
||||
* @param configDirectory
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
private void updateClientDateFormat(File baseDirectory, File configDirectory)
|
||||
{
|
||||
File configFile = new File(configDirectory, "config.xml");
|
||||
|
||||
if (configFile.exists())
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
// backup config.xml
|
||||
File backupDirectory = createBackupDirectory(baseDirectory,
|
||||
"upgrade to version {0}");
|
||||
|
||||
Files.copy(configFile, new File(backupDirectory, "config.xml"));
|
||||
|
||||
// change dateformat
|
||||
|
||||
DocumentBuilder builder =
|
||||
DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
Document document = builder.parse(configFile);
|
||||
|
||||
fixClientDateFormat(document);
|
||||
writeDocument(document, configFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.error("could not parse document", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.upgrade;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.version.Version;
|
||||
import sonia.scm.util.IOUtil;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import java.text.ParseException;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.transform.TransformerConfigurationException;
|
||||
import javax.xml.transform.TransformerException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class TimestampUpgradeHandler extends XmlUpgradeHandler
|
||||
{
|
||||
|
||||
/**
|
||||
* the logger for TimestampUpgradeHandler
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(TimestampUpgradeHandler.class);
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param homeDirectory
|
||||
* @param configDirectory
|
||||
* @param oldVersion
|
||||
* @param newVersion
|
||||
*/
|
||||
@Override
|
||||
public void doUpgrade(File homeDirectory, File configDirectory,
|
||||
Version oldVersion, Version newVersion)
|
||||
{
|
||||
if (oldVersion.isOlder("1.2"))
|
||||
{
|
||||
if (logger.isInfoEnabled())
|
||||
{
|
||||
logger.info("data format is older than 1.2, upgrade to version {}",
|
||||
SCMContext.getContext().getVersion());
|
||||
}
|
||||
|
||||
fixDate(homeDirectory, configDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param value
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private String convertDate(String value)
|
||||
{
|
||||
if (!Strings.isNullOrEmpty(value))
|
||||
{
|
||||
try
|
||||
{
|
||||
Date date = Util.parseDate(value);
|
||||
|
||||
if (date != null)
|
||||
{
|
||||
value = Long.toString(date.getTime());
|
||||
}
|
||||
}
|
||||
catch (ParseException ex)
|
||||
{
|
||||
logger.warn("could not parse date", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param baseDirectory
|
||||
* @param configDirectory
|
||||
*/
|
||||
private void fixDate(File baseDirectory, File configDirectory)
|
||||
{
|
||||
try
|
||||
{
|
||||
DocumentBuilder builder =
|
||||
DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
File backupDirectory = createBackupDirectory(baseDirectory,
|
||||
"upgrade to version {0}");
|
||||
|
||||
fixDate(builder, configDirectory, backupDirectory, "users.xml");
|
||||
fixDate(builder, configDirectory, backupDirectory, "groups.xml");
|
||||
fixDate(builder, configDirectory, backupDirectory, "repositories.xml");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.error("could not parse document", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param builder
|
||||
* @param configDirectory
|
||||
* @param backupDirectory
|
||||
* @param filename
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws SAXException
|
||||
* @throws TransformerConfigurationException
|
||||
* @throws TransformerException
|
||||
*/
|
||||
private void fixDate(DocumentBuilder builder, File configDirectory,
|
||||
File backupDirectory, String filename)
|
||||
throws SAXException, IOException, TransformerConfigurationException,
|
||||
TransformerException
|
||||
{
|
||||
File configFile = new File(configDirectory, filename);
|
||||
File backupFile = new File(backupDirectory, filename);
|
||||
|
||||
IOUtil.copy(configFile, backupFile);
|
||||
|
||||
if (configFile.exists())
|
||||
{
|
||||
if (logger.isInfoEnabled())
|
||||
{
|
||||
logger.info("fix date elements of {}", configFile.getPath());
|
||||
}
|
||||
|
||||
Document document = builder.parse(configFile);
|
||||
|
||||
fixDate(document, "lastModified");
|
||||
fixDate(document, "creationDate");
|
||||
fixDate(document, "creationTime");
|
||||
writeDocument(document, configFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param document
|
||||
* @param element
|
||||
*/
|
||||
private void fixDate(Document document, String element)
|
||||
{
|
||||
NodeList nodes = document.getElementsByTagName(element);
|
||||
|
||||
if (nodes != null)
|
||||
{
|
||||
for (int i = 0; i < nodes.getLength(); i++)
|
||||
{
|
||||
Node node = nodes.item(i);
|
||||
String value = node.getTextContent();
|
||||
|
||||
value = convertDate(value);
|
||||
node.setTextContent(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package sonia.scm.upgrade;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.version.Version;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class UpgradeManager
|
||||
{
|
||||
|
||||
/** the logger for ScmUpgradeHandler */
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(UpgradeManager.class);
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public void doUpgrade()
|
||||
{
|
||||
File baseDirectory = SCMContext.getContext().getBaseDirectory();
|
||||
File configDirectory = new File(baseDirectory, "config");
|
||||
File versionFile = new File(configDirectory, "version.txt");
|
||||
|
||||
if (configDirectory.exists())
|
||||
{
|
||||
boolean writeVersionFile = false;
|
||||
|
||||
String newVersion = SCMContext.getContext().getVersion();
|
||||
|
||||
if (versionFile.exists())
|
||||
{
|
||||
|
||||
String oldVersion = getVersionString(versionFile);
|
||||
|
||||
if (!Strings.isNullOrEmpty(oldVersion) &&!oldVersion.equals(newVersion))
|
||||
{
|
||||
if (!newVersion.equals(oldVersion))
|
||||
{
|
||||
writeVersionFile = doUpgradesForOldVersion(baseDirectory,
|
||||
configDirectory, oldVersion, newVersion);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
writeVersionFile = doUpgradesForOldVersion(baseDirectory,
|
||||
configDirectory, "1.1", newVersion);
|
||||
}
|
||||
|
||||
if (writeVersionFile)
|
||||
{
|
||||
writeVersionFile(versionFile);
|
||||
logger.info("upgrade to version {} was successful", newVersion);
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
// fresh installation
|
||||
IOUtil.mkdirs(configDirectory);
|
||||
writeVersionFile(versionFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private List<UpgradeHandler> collectUpgradeHandlers()
|
||||
{
|
||||
|
||||
List<UpgradeHandler> upgradeHandlers = Lists.newArrayList();
|
||||
|
||||
upgradeHandlers.add(new TimestampUpgradeHandler());
|
||||
upgradeHandlers.add(new ClientDateFormatUpgradeHandler());
|
||||
|
||||
// TODO find upgrade handlers on classpath
|
||||
return upgradeHandlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param baseDirectory
|
||||
* @param configDirectory
|
||||
* @param versionString
|
||||
* @param oldVersionString
|
||||
* @param newVersionString
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private boolean doUpgradesForOldVersion(File baseDirectory,
|
||||
File configDirectory, String oldVersionString, String newVersionString)
|
||||
{
|
||||
logger.info("start upgrade from version \"{}\" to \"{}\"",
|
||||
oldVersionString, newVersionString);
|
||||
|
||||
boolean writeVersionFile = false;
|
||||
|
||||
try
|
||||
{
|
||||
Version oldVersion = Version.parse(oldVersionString);
|
||||
Version newVersion = Version.parse(newVersionString);
|
||||
|
||||
doUpgradesForOldVersion(baseDirectory, configDirectory, oldVersion,
|
||||
newVersion);
|
||||
writeVersionFile = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.error("error upgrade failed", ex);
|
||||
}
|
||||
|
||||
return writeVersionFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param baseDirectory
|
||||
* @param configDirectory
|
||||
* @param version
|
||||
* @param oldVersion
|
||||
* @param newVersion
|
||||
*/
|
||||
private void doUpgradesForOldVersion(File baseDirectory,
|
||||
File configDirectory, Version oldVersion, Version newVersion)
|
||||
{
|
||||
List<UpgradeHandler> upgradeHandlers = collectUpgradeHandlers();
|
||||
|
||||
for (UpgradeHandler upgradeHandler : upgradeHandlers)
|
||||
{
|
||||
logger.trace("call upgrade handler {}", upgradeHandler.getClass());
|
||||
upgradeHandler.doUpgrade(baseDirectory, configDirectory, oldVersion,
|
||||
newVersion);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param versionFile
|
||||
*/
|
||||
private void writeVersionFile(File versionFile)
|
||||
{
|
||||
OutputStream output = null;
|
||||
|
||||
try
|
||||
{
|
||||
output = new FileOutputStream(versionFile);
|
||||
output.write(SCMContext.getContext().getVersion().getBytes());
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
logger.error("could not write version file", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IOUtil.close(output);
|
||||
}
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param versionFile
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private String getVersionString(File versionFile)
|
||||
{
|
||||
String version = null;
|
||||
|
||||
try
|
||||
{
|
||||
version = Files.toString(versionFile, Charsets.UTF_8).trim();
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
logger.error("could not read version file", ex);
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.upgrade;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerConfigurationException;
|
||||
import javax.xml.transform.TransformerException;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public abstract class XmlUpgradeHandler implements UpgradeHandler
|
||||
{
|
||||
|
||||
/**
|
||||
* the logger for XmlUpgradeHandler
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(XmlUpgradeHandler.class);
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param baseDirectory
|
||||
* @param note
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
protected File createBackupDirectory(File baseDirectory, String note)
|
||||
{
|
||||
String date = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
File backupDirectory = new File(baseDirectory,
|
||||
"backups".concat(File.separator).concat(date));
|
||||
|
||||
IOUtil.mkdirs(backupDirectory);
|
||||
|
||||
FileWriter writer = null;
|
||||
|
||||
note = MessageFormat.format(note, SCMContext.getContext().getVersion());
|
||||
|
||||
try
|
||||
{
|
||||
writer = new FileWriter(new File(backupDirectory, "note.txt"));
|
||||
writer.write(note);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
logger.error("could not write note.txt for backup", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IOUtil.close(writer);
|
||||
}
|
||||
|
||||
return backupDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param document
|
||||
* @param configFile
|
||||
*
|
||||
* @throws TransformerConfigurationException
|
||||
* @throws TransformerException
|
||||
*/
|
||||
protected void writeDocument(Document document, File configFile)
|
||||
throws TransformerConfigurationException, TransformerException
|
||||
{
|
||||
Transformer transformer = TransformerFactory.newInstance().newTransformer();
|
||||
|
||||
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
|
||||
transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
|
||||
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
|
||||
transformer.transform(new DOMSource(document),
|
||||
new StreamResult(configFile));
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@
|
||||
<value>configuration:read,write:*</value>
|
||||
</permission>
|
||||
<permission>
|
||||
<value>repositoryRole:read,write</value>
|
||||
<value>repositoryRole:write</value>
|
||||
</permission>
|
||||
|
||||
</permissions>
|
||||
|
||||
@@ -89,8 +89,7 @@ class DefaultRepositoryRoleManagerTest {
|
||||
|
||||
@BeforeEach
|
||||
void authorizeUser() {
|
||||
when(subject.isPermitted("repositoryRole:read")).thenReturn(true);
|
||||
when(subject.isPermitted("repositoryRole:modify")).thenReturn(true);
|
||||
when(subject.isPermitted("repositoryRole:write")).thenReturn(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -184,8 +183,15 @@ class DefaultRepositoryRoleManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowException_forGet() {
|
||||
assertThrows(UnauthorizedException.class, () -> manager.get("any"));
|
||||
void shouldReturnNull_forNotExistingRole() {
|
||||
RepositoryRole role = manager.get("noSuchRole");
|
||||
assertThat(role).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnRole_forExistingRole() {
|
||||
RepositoryRole role = manager.get(CUSTOM_ROLE_NAME);
|
||||
assertThat(role).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -201,18 +207,25 @@ class DefaultRepositoryRoleManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyList() {
|
||||
assertThat(manager.getAll()).isEmpty();
|
||||
void shouldReturnAllRoles() {
|
||||
List<RepositoryRole> allRoles = manager.getAll();
|
||||
assertThat(allRoles).containsExactly(CUSTOM_ROLE, SYSTEM_ROLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyFilteredList() {
|
||||
assertThat(manager.getAll(x -> true, null)).isEmpty();
|
||||
void shouldReturnFilteredList() {
|
||||
Collection<RepositoryRole> allRoles = manager.getAll(role -> CUSTOM_ROLE_NAME.equals(role.getName()), null);
|
||||
assertThat(allRoles).containsExactly(CUSTOM_ROLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyPaginatedList() {
|
||||
assertThat(manager.getAll(1, 1)).isEmpty();
|
||||
void shouldReturnPaginatedRoles() {
|
||||
Collection<RepositoryRole> allRoles =
|
||||
manager.getAll(
|
||||
Comparator.comparing(RepositoryRole::getType),
|
||||
1, 1
|
||||
);
|
||||
assertThat(allRoles).containsExactly(CUSTOM_ROLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package sonia.scm.update;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import org.mockito.Mockito;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.security.AssignedPermission;
|
||||
import sonia.scm.security.DefaultKeyGenerator;
|
||||
import sonia.scm.store.ConfigurationEntryStore;
|
||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
|
||||
public class UpdateStepTestUtil {
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
|
||||
private final Path tempDir;
|
||||
private final ConfigurationEntryStoreFactory storeFactory;
|
||||
|
||||
public UpdateStepTestUtil(Path tempDir) {
|
||||
this.tempDir = tempDir;
|
||||
contextProvider = Mockito.mock(SCMContextProvider.class);
|
||||
storeFactory = new JAXBConfigurationEntryStoreFactory(contextProvider, null, new DefaultKeyGenerator());
|
||||
lenient().when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
lenient().when(contextProvider.resolve(any())).thenAnswer(invocation -> tempDir.resolve(invocation.getArgument(0).toString()));
|
||||
}
|
||||
|
||||
public SCMContextProvider getContextProvider() {
|
||||
return contextProvider;
|
||||
}
|
||||
|
||||
public ConfigurationEntryStoreFactory getStoreFactory() {
|
||||
return storeFactory;
|
||||
}
|
||||
|
||||
public void copyConfigFile(String fileName) throws IOException {
|
||||
Path configDir = tempDir.resolve("config");
|
||||
Files.createDirectories(configDir);
|
||||
copyTestDatabaseFile(configDir, fileName);
|
||||
}
|
||||
|
||||
public void copyConfigFile(String fileName, String targetFileName) throws IOException {
|
||||
Path configDir = tempDir.resolve("config");
|
||||
Files.createDirectories(configDir);
|
||||
copyTestDatabaseFile(configDir, fileName, targetFileName);
|
||||
}
|
||||
|
||||
public ConfigurationEntryStore<AssignedPermission> getStoreForConfigFile(String name) {
|
||||
return storeFactory
|
||||
.withType(AssignedPermission.class)
|
||||
.withName(name)
|
||||
.build();
|
||||
}
|
||||
|
||||
public Path getFile(String name) {
|
||||
return tempDir.resolve("config").resolve(name);
|
||||
}
|
||||
|
||||
private void copyTestDatabaseFile(Path configDir, String fileName) throws IOException {
|
||||
Path targetFileName = Paths.get(fileName).getFileName();
|
||||
copyTestDatabaseFile(configDir, fileName, targetFileName.toString());
|
||||
}
|
||||
|
||||
private void copyTestDatabaseFile(Path configDir, String fileName, String targetFileName) throws IOException {
|
||||
URL url = Resources.getResource(fileName);
|
||||
Files.copy(url.openStream(), configDir.resolve(targetFileName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package sonia.scm.update.group;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.xml.XmlGroupDAO;
|
||||
import sonia.scm.update.UpdateStepTestUtil;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.linesOf;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@ExtendWith(TempDirectory.class)
|
||||
class XmlGroupV1UpdateStepTest {
|
||||
|
||||
@Mock
|
||||
XmlGroupDAO groupDAO;
|
||||
|
||||
@Captor
|
||||
ArgumentCaptor<Group> groupCaptor;
|
||||
|
||||
XmlGroupV1UpdateStep updateStep;
|
||||
|
||||
private UpdateStepTestUtil testUtil;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
|
||||
testUtil = new UpdateStepTestUtil(tempDir);
|
||||
updateStep = new XmlGroupV1UpdateStep(testUtil.getContextProvider(), groupDAO, testUtil.getStoreFactory());
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithExistingDatabase {
|
||||
|
||||
@BeforeEach
|
||||
void captureStoredRepositories() {
|
||||
doNothing().when(groupDAO).add(groupCaptor.capture());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void createGroupV1XML() throws IOException {
|
||||
testUtil.copyConfigFile("sonia/scm/update/group/groups.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateNewGroupFromGroupsV1Xml() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
verify(groupDAO, times(2)).add(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapAttributesFromGroupsV1Xml() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
Optional<Group> group = groupCaptor.getAllValues().stream().filter(u -> u.getName().equals("normals")).findFirst();
|
||||
assertThat(group)
|
||||
.get()
|
||||
.hasFieldOrPropertyWithValue("name", "normals")
|
||||
.hasFieldOrPropertyWithValue("description", "Normal people")
|
||||
.hasFieldOrPropertyWithValue("type", "xml")
|
||||
.hasFieldOrPropertyWithValue("members", asList("trillian", "dent"))
|
||||
.hasFieldOrPropertyWithValue("lastModified", 1559550955883L)
|
||||
.hasFieldOrPropertyWithValue("creationDate", 1559548942457L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExtractProperties() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
Path propertiesFile = testUtil.getFile("group-properties-v1.xml");
|
||||
assertThat(propertiesFile)
|
||||
.exists();
|
||||
assertThat(linesOf(propertiesFile.toFile()))
|
||||
.extracting(String::trim)
|
||||
.containsSequence(
|
||||
"<key>normals</key>",
|
||||
"<value>",
|
||||
"<item>",
|
||||
"<key>mostly</key>",
|
||||
"<value>humans</value>",
|
||||
"</item>",
|
||||
"</value>");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithExistingDatabaseWithEmptyList {
|
||||
|
||||
@BeforeEach
|
||||
void createGroupV1XML() throws IOException {
|
||||
testUtil.copyConfigFile("sonia/scm/update/group/groups_empty_groups.xml", "groups.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateNewGroupFromGroupsV1Xml() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
verify(groupDAO, times(0)).add(any());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithExistingDatabaseWithoutList {
|
||||
|
||||
@BeforeEach
|
||||
void createGroupV1XML() throws IOException {
|
||||
testUtil.copyConfigFile("sonia/scm/update/group/groups_no_groups.xml", "groups.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateNewGroupFromGroupsV1Xml() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
verify(groupDAO, times(0)).add(any());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotFailForMissingConfigDir() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(TempDirectory.class)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CopyMigrationStrategyTest {
|
||||
|
||||
@Mock
|
||||
SCMContextProvider contextProvider;
|
||||
@Mock
|
||||
RepositoryLocationResolver locationResolver;
|
||||
|
||||
@BeforeEach
|
||||
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
|
||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
V1RepositoryFileSystem.createV1Home(tempDir);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
|
||||
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
|
||||
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
|
||||
when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
||||
assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCopyDataDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
||||
assertThat(target.resolve("data")).exists();
|
||||
Path originalDataDir = tempDir
|
||||
.resolve("repositories")
|
||||
.resolve("git")
|
||||
.resolve("some")
|
||||
.resolve("more")
|
||||
.resolve("directories")
|
||||
.resolve("than")
|
||||
.resolve("one");
|
||||
assertDirectoriesEqual(target.resolve("data"), originalDataDir);
|
||||
}
|
||||
|
||||
private void assertDirectoriesEqual(Path targetDataDir, Path originalDataDir) {
|
||||
Stream<Path> list = null;
|
||||
try {
|
||||
list = Files.list(originalDataDir);
|
||||
} catch (IOException e) {
|
||||
fail("could not read original directory", e);
|
||||
}
|
||||
list.forEach(
|
||||
original -> {
|
||||
Path expectedTarget = targetDataDir.resolve(original.getFileName());
|
||||
assertThat(expectedTarget).exists();
|
||||
if (Files.isDirectory(original)) {
|
||||
assertDirectoriesEqual(expectedTarget, original);
|
||||
} else {
|
||||
assertThat(expectedTarget).hasSameContentAs(original);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(TempDirectory.class)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InlineMigrationStrategyTest {
|
||||
|
||||
@Mock
|
||||
SCMContextProvider contextProvider;
|
||||
|
||||
@BeforeEach
|
||||
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
|
||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
V1RepositoryFileSystem.createV1Home(tempDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseExistingDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||
Path target = new InlineMigrationStrategy(contextProvider).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
||||
assertThat(target).isEqualTo(resolveOldDirectory(tempDir));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||
new InlineMigrationStrategy(contextProvider).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
||||
assertThat(resolveOldDirectory(tempDir).resolve("data")).exists();
|
||||
}
|
||||
|
||||
private Path resolveOldDirectory(Path tempDir) {
|
||||
return tempDir.resolve("repositories").resolve("git").resolve("some").resolve("more").resolve("directories").resolve("than").resolve("one");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package sonia.scm.repository.update;
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import org.assertj.core.api.Assertions;
|
||||
@@ -42,7 +42,7 @@ class MigrateVerbsToPermissionRolesTest {
|
||||
|
||||
@BeforeEach
|
||||
void init(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
URL metadataUrl = Resources.getResource("sonia/scm/repository/update/metadataWithoutRoles.xml");
|
||||
URL metadataUrl = Resources.getResource("sonia/scm/update/repository/metadataWithoutRoles.xml");
|
||||
Files.copy(metadataUrl.openStream(), tempDir.resolve("metadata.xml"));
|
||||
doAnswer(invocation -> {
|
||||
((BiConsumer<String, Path>) invocation.getArgument(0)).accept(EXISTING_REPOSITORY_ID, tempDir);
|
||||
@@ -0,0 +1,77 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.store.ConfigurationStoreFactory;
|
||||
import sonia.scm.store.JAXBConfigurationStoreFactory;
|
||||
import sonia.scm.update.repository.MigrationStrategy;
|
||||
import sonia.scm.update.repository.MigrationStrategyDao;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.update.repository.MigrationStrategy.INLINE;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@ExtendWith(TempDirectory.class)
|
||||
class MigrationStrategyDaoTest {
|
||||
|
||||
@Mock
|
||||
SCMContextProvider contextProvider;
|
||||
|
||||
private ConfigurationStoreFactory storeFactory;
|
||||
|
||||
@BeforeEach
|
||||
void initStore(@TempDirectory.TempDir Path tempDir) {
|
||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
storeFactory = new JAXBConfigurationStoreFactory(contextProvider, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyOptionalWhenStoreIsEmpty() throws JAXBException {
|
||||
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||
|
||||
Optional<MigrationStrategy> strategy = dao.get("any");
|
||||
|
||||
Assertions.assertThat(strategy).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNewValue() throws JAXBException {
|
||||
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||
|
||||
dao.set("id", INLINE);
|
||||
|
||||
Optional<MigrationStrategy> strategy = dao.get("id");
|
||||
|
||||
Assertions.assertThat(strategy).contains(INLINE);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithExistingDatabase {
|
||||
@BeforeEach
|
||||
void initExistingDatabase() throws JAXBException {
|
||||
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||
|
||||
dao.set("id", INLINE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindExistingValue() throws JAXBException {
|
||||
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||
|
||||
Optional<MigrationStrategy> strategy = dao.get("id");
|
||||
|
||||
Assertions.assertThat(strategy).contains(INLINE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import com.google.inject.Injector;
|
||||
import sonia.scm.update.repository.MigrationStrategy.Instance;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MigrationStrategyMock {
|
||||
|
||||
static Injector init() {
|
||||
Map<Class, Instance> mocks = new HashMap<>();
|
||||
Injector mock = mock(Injector.class);
|
||||
when(
|
||||
mock.getInstance(any(Class.class)))
|
||||
.thenAnswer(
|
||||
invocationOnMock -> mocks.computeIfAbsent(invocationOnMock.getArgument(0), key -> mock((Class<Instance>) key))
|
||||
);
|
||||
return mock;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(TempDirectory.class)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MoveMigrationStrategyTest {
|
||||
|
||||
@Mock
|
||||
SCMContextProvider contextProvider;
|
||||
@Mock
|
||||
RepositoryLocationResolver locationResolver;
|
||||
|
||||
@BeforeEach
|
||||
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
|
||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
V1RepositoryFileSystem.createV1Home(tempDir);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
|
||||
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
|
||||
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
|
||||
when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
||||
assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
||||
assertThat(target.resolve("data")).exists();
|
||||
Path originalDataDir = tempDir
|
||||
.resolve("repositories")
|
||||
.resolve("git")
|
||||
.resolve("some");
|
||||
assertThat(originalDataDir).doesNotExist();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import sonia.scm.repository.spi.ZippedRepositoryTestBase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
class V1RepositoryFileSystem {
|
||||
/**
|
||||
* Creates the following v1 repositories in the temp dir:
|
||||
* <pre>
|
||||
* <repository>
|
||||
* <properties/>
|
||||
* <contact>arthur@dent.uk</contact>
|
||||
* <creationDate>1558423492071</creationDate>
|
||||
* <description>A repository with two folders.</description>
|
||||
* <id>3b91caa5-59c3-448f-920b-769aaa56b761</id>
|
||||
* <name>one/directory</name>
|
||||
* <public>false</public>
|
||||
* <archived>false</archived>
|
||||
* <type>git</type>
|
||||
* </repository>
|
||||
* <repository>
|
||||
* <properties/>
|
||||
* <contact>arthur@dent.uk</contact>
|
||||
* <creationDate>1558423543716</creationDate>
|
||||
* <description>A repository in deeply nested folders.</description>
|
||||
* <id>c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f</id>
|
||||
* <name>some/more/directories/than/one</name>
|
||||
* <public>false</public>
|
||||
* <archived>false</archived>
|
||||
* <type>git</type>
|
||||
* </repository>
|
||||
* <repository>
|
||||
* <properties/>
|
||||
* <contact>arthur@dent.uk</contact>
|
||||
* <creationDate>1558423440258</creationDate>
|
||||
* <description>A simple repository without directories.</description>
|
||||
* <id>454972da-faf9-4437-b682-dc4a4e0aa8eb</id>
|
||||
* <lastModified>1558425918578</lastModified>
|
||||
* <name>simple</name>
|
||||
* <permissions>
|
||||
* <groupPermission>true</groupPermission>
|
||||
* <name>mice</name>
|
||||
* <type>WRITE</type>
|
||||
* </permissions>
|
||||
* <permissions>
|
||||
* <groupPermission>false</groupPermission>
|
||||
* <name>dent</name>
|
||||
* <type>OWNER</type>
|
||||
* </permissions>
|
||||
* <permissions>
|
||||
* <groupPermission>false</groupPermission>
|
||||
* <name>trillian</name>
|
||||
* <type>READ</type>
|
||||
* </permissions>
|
||||
* <public>false</public>
|
||||
* <archived>false</archived>
|
||||
* <type>git</type>
|
||||
* <url>http://localhost:8081/scm/git/simple</url>
|
||||
* </repository>
|
||||
* </pre>
|
||||
*/
|
||||
static void createV1Home(Path tempDir) throws IOException {
|
||||
ZippedRepositoryTestBase.extract(tempDir.toFile(), "sonia/scm/update/repository/scm-home.v1.zip");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
||||
import sonia.scm.repository.xml.XmlRepositoryDAO;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(TempDirectory.class)
|
||||
class XmlRepositoryFileNameUpdateStepTest {
|
||||
|
||||
SCMContextProvider contextProvider = mock(SCMContextProvider.class);
|
||||
XmlRepositoryDAO repositoryDAO = mock(XmlRepositoryDAO.class);
|
||||
|
||||
@BeforeEach
|
||||
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
|
||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCopyRepositoriesFileToRepositoryPathsFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
XmlRepositoryFileNameUpdateStep updateStep = new XmlRepositoryFileNameUpdateStep(contextProvider, repositoryDAO);
|
||||
URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml");
|
||||
Path configDir = tempDir.resolve("config");
|
||||
Files.createDirectories(configDir);
|
||||
Files.copy(url.openStream(), configDir.resolve("repositories.xml"));
|
||||
|
||||
updateStep.doUpdate();
|
||||
|
||||
assertThat(configDir.resolve(PathBasedRepositoryLocationResolver.STORE_NAME + ".xml")).exists();
|
||||
assertThat(configDir.resolve("repositories.xml")).doesNotExist();
|
||||
verify(repositoryDAO).refresh();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package sonia.scm.update.repository;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.inject.Injector;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.xml.XmlRepositoryDAO;
|
||||
import sonia.scm.store.ConfigurationEntryStore;
|
||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.InMemoryConfigurationEntryStore;
|
||||
import sonia.scm.store.InMemoryConfigurationEntryStoreFactory;
|
||||
import sonia.scm.update.UpdateStepTestUtil;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.update.repository.MigrationStrategy.COPY;
|
||||
import static sonia.scm.update.repository.MigrationStrategy.INLINE;
|
||||
import static sonia.scm.update.repository.MigrationStrategy.MOVE;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@ExtendWith(TempDirectory.class)
|
||||
class XmlRepositoryV1UpdateStepTest {
|
||||
|
||||
Injector injectorMock = MigrationStrategyMock.init();
|
||||
|
||||
@Mock
|
||||
XmlRepositoryDAO repositoryDAO;
|
||||
@Mock
|
||||
MigrationStrategyDao migrationStrategyDao;
|
||||
|
||||
ConfigurationEntryStoreFactory configurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(new InMemoryConfigurationEntryStore());
|
||||
|
||||
@Captor
|
||||
ArgumentCaptor<Repository> storeCaptor;
|
||||
@Captor
|
||||
ArgumentCaptor<Path> locationCaptor;
|
||||
|
||||
UpdateStepTestUtil testUtil;
|
||||
|
||||
XmlRepositoryV1UpdateStep updateStep;
|
||||
|
||||
@BeforeEach
|
||||
void createUpdateStepFromMocks(@TempDirectory.TempDir Path tempDir) {
|
||||
testUtil = new UpdateStepTestUtil(tempDir);
|
||||
updateStep = new XmlRepositoryV1UpdateStep(
|
||||
testUtil.getContextProvider(),
|
||||
repositoryDAO,
|
||||
migrationStrategyDao,
|
||||
injectorMock,
|
||||
configurationEntryStoreFactory
|
||||
);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithExistingDatabase {
|
||||
|
||||
@BeforeEach
|
||||
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
V1RepositoryFileSystem.createV1Home(tempDir);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void captureStoredRepositories() {
|
||||
lenient().doNothing().when(repositoryDAO).add(storeCaptor.capture(), locationCaptor.capture());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void createMigrationPlan() {
|
||||
lenient().when(migrationStrategyDao.get("3b91caa5-59c3-448f-920b-769aaa56b761")).thenReturn(of(MOVE));
|
||||
lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(of(COPY));
|
||||
lenient().when(migrationStrategyDao.get("454972da-faf9-4437-b682-dc4a4e0aa8eb")).thenReturn(of(INLINE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateNewRepositories() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
verify(repositoryDAO, times(3)).add(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapAttributes() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
|
||||
Optional<Repository> repository = findByNamespace("git");
|
||||
|
||||
assertThat(repository)
|
||||
.get()
|
||||
.hasFieldOrPropertyWithValue("type", "git")
|
||||
.hasFieldOrPropertyWithValue("contact", "arthur@dent.uk")
|
||||
.hasFieldOrPropertyWithValue("description", "A simple repository without directories.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseRepositoryTypeAsNamespaceForNamesWithSingleElement() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
|
||||
Optional<Repository> repository = findByNamespace("git");
|
||||
|
||||
assertThat(repository)
|
||||
.get()
|
||||
.hasFieldOrPropertyWithValue("namespace", "git")
|
||||
.hasFieldOrPropertyWithValue("name", "simple");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseDirectoriesForNamespaceAndNameForNamesWithTwoElements() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
|
||||
Optional<Repository> repository = findByNamespace("one");
|
||||
|
||||
assertThat(repository)
|
||||
.get()
|
||||
.hasFieldOrPropertyWithValue("namespace", "one")
|
||||
.hasFieldOrPropertyWithValue("name", "directory");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseDirectoriesForNamespaceAndConcatenatedNameForNamesWithMoreThanTwoElements() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
|
||||
Optional<Repository> repository = findByNamespace("some");
|
||||
|
||||
assertThat(repository)
|
||||
.get()
|
||||
.hasFieldOrPropertyWithValue("namespace", "some")
|
||||
.hasFieldOrPropertyWithValue("name", "more_directories_than_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapPermissions() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
|
||||
Optional<Repository> repository = findByNamespace("git");
|
||||
|
||||
assertThat(repository.get().getPermissions())
|
||||
.hasSize(3)
|
||||
.contains(
|
||||
new RepositoryPermission("mice", "WRITE", true),
|
||||
new RepositoryPermission("dent", "OWNER", false),
|
||||
new RepositoryPermission("trillian", "READ", false)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExtractPropertiesFromRepositories() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
|
||||
ConfigurationEntryStore<Object> store = configurationEntryStoreFactory.withType(null).withName("").build();
|
||||
assertThat(store.getAll())
|
||||
.hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseDirectoryFromStrategy(@TempDirectory.TempDir Path tempDir) throws JAXBException {
|
||||
Path targetDir = tempDir.resolve("someDir");
|
||||
MigrationStrategy.Instance strategyMock = injectorMock.getInstance(InlineMigrationStrategy.class);
|
||||
when(strategyMock.migrate("454972da-faf9-4437-b682-dc4a4e0aa8eb", "simple", "git")).thenReturn(targetDir);
|
||||
|
||||
updateStep.doUpdate();
|
||||
|
||||
assertThat(locationCaptor.getAllValues()).contains(targetDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailForMissingMigrationStrategy() {
|
||||
lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(empty());
|
||||
assertThrows(IllegalStateException.class, () -> updateStep.doUpdate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBackupOldRepositoryDatabaseFile(@TempDirectory.TempDir Path tempDir) throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
|
||||
assertThat(tempDir.resolve("config").resolve("repositories.xml")).doesNotExist();
|
||||
assertThat(tempDir.resolve("config").resolve("repositories.xml.v1.backup")).exists();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotFailIfNoOldDatabaseExists() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotFailIfFormerV2DatabaseExists(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException {
|
||||
createFormerV2RepositoriesFile(tempDir);
|
||||
|
||||
updateStep.doUpdate();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotBackupFormerV2DatabaseFile(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException {
|
||||
createFormerV2RepositoriesFile(tempDir);
|
||||
|
||||
updateStep.doUpdate();
|
||||
|
||||
assertThat(tempDir.resolve("config").resolve("repositories.xml")).exists();
|
||||
assertThat(tempDir.resolve("config").resolve("repositories.xml.v1.backup")).doesNotExist();
|
||||
}
|
||||
|
||||
private void createFormerV2RepositoriesFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml");
|
||||
Path configDir = tempDir.resolve("config");
|
||||
Files.createDirectories(configDir);
|
||||
Files.copy(url.openStream(), configDir.resolve("repositories.xml"));
|
||||
}
|
||||
|
||||
private Optional<Repository> findByNamespace(String namespace) {
|
||||
return storeCaptor.getAllValues().stream().filter(r -> r.getNamespace().equals(namespace)).findFirst();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package sonia.scm.update.security;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.security.AssignedPermission;
|
||||
import sonia.scm.store.ConfigurationEntryStore;
|
||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.InMemoryConfigurationEntryStore;
|
||||
import sonia.scm.store.InMemoryConfigurationEntryStoreFactory;
|
||||
import sonia.scm.update.security.XmlSecurityV1UpdateStep;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@ExtendWith(TempDirectory.class)
|
||||
class XmlSecurityV1UpdateStepTest {
|
||||
|
||||
@Mock
|
||||
SCMContextProvider contextProvider;
|
||||
|
||||
XmlSecurityV1UpdateStep updateStep;
|
||||
ConfigurationEntryStore<AssignedPermission> assignedPermissionStore;
|
||||
|
||||
@BeforeEach
|
||||
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
|
||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
assignedPermissionStore = new InMemoryConfigurationEntryStore<>();
|
||||
ConfigurationEntryStoreFactory inMemoryConfigurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(assignedPermissionStore);
|
||||
updateStep = new XmlSecurityV1UpdateStep(contextProvider, inMemoryConfigurationEntryStoreFactory);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithExistingDatabase {
|
||||
|
||||
@BeforeEach
|
||||
void createConfigV1XML(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
Path configDir = tempDir.resolve("config");
|
||||
Files.createDirectories(configDir);
|
||||
copyTestDatabaseFile(configDir, "config.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreatePermissionForUsersConfiguredAsAdmin() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
List<String> assignedPermission =
|
||||
assignedPermissionStore.getAll().values()
|
||||
.stream()
|
||||
.filter(a -> a.getPermission().getValue().equals("*"))
|
||||
.filter(a -> !a.isGroupPermission())
|
||||
.map(AssignedPermission::getName)
|
||||
.collect(toList());
|
||||
assertThat(assignedPermission).contains("arthur", "dent", "ldap-admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreatePermissionForGroupsConfiguredAsAdmin() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
List<String> assignedPermission =
|
||||
assignedPermissionStore.getAll().values()
|
||||
.stream()
|
||||
.filter(a -> a.getPermission().getValue().equals("*"))
|
||||
.filter(AssignedPermission::isGroupPermission)
|
||||
.map(AssignedPermission::getName)
|
||||
.collect(toList());
|
||||
assertThat(assignedPermission).contains("admins", "vogons");
|
||||
}
|
||||
}
|
||||
|
||||
private void copyTestDatabaseFile(Path configDir, String fileName) throws IOException {
|
||||
URL url = Resources.getResource("sonia/scm/update/security/" + fileName);
|
||||
Files.copy(url.openStream(), configDir.resolve(fileName));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotFailForMissingConfigDir() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user