diff --git a/gradle/changelog/namespace_update_steps.yaml b/gradle/changelog/namespace_update_steps.yaml
new file mode 100644
index 0000000000..03360527b3
--- /dev/null
+++ b/gradle/changelog/namespace_update_steps.yaml
@@ -0,0 +1,2 @@
+- type: added
+ description: Update steps for namespaces
diff --git a/scm-core/src/main/java/sonia/scm/migration/NamespaceUpdateContext.java b/scm-core/src/main/java/sonia/scm/migration/NamespaceUpdateContext.java
new file mode 100644
index 0000000000..5dd8364b6c
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/migration/NamespaceUpdateContext.java
@@ -0,0 +1,46 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.migration;
+
+/**
+ * Data for the namespace, whose data that should be migrated.
+ *
+ * @since 2.47.0
+ */
+public final class NamespaceUpdateContext {
+
+ private final String namespace;
+
+ public NamespaceUpdateContext(String namespace) {
+ this.namespace = namespace;
+ }
+
+ /**
+ * The name of the namespace, whose data should be migrated.
+ */
+ public String getNamespace() {
+ return namespace;
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/migration/NamespaceUpdateStep.java b/scm-core/src/main/java/sonia/scm/migration/NamespaceUpdateStep.java
new file mode 100644
index 0000000000..78be05d79a
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/migration/NamespaceUpdateStep.java
@@ -0,0 +1,52 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.migration;
+
+import sonia.scm.plugin.ExtensionPoint;
+
+/**
+ * This is the main interface for "namespace specific" data migration/update. Using this interface, SCM-Manager
+ * provides the possibility to change data structures between versions for a given type of data. This class should be
+ * used only for namespace specific data (eg. the store is created with a call of forNamespace in a store
+ * factory. To migrate global data, use a {@link UpdateStep}.
+ *
For information about {@link #getAffectedDataType()} and {@link #getTargetVersion()}, see the package
+ * documentation.
+ *
+ * @see sonia.scm.migration
+ *
+ * @since 2.47.0
+ */
+@ExtensionPoint
+public interface NamespaceUpdateStep extends UpdateStepTarget {
+ /**
+ * Implement this to update the data to the new version for a specific namespace. If any {@link Exception} is thrown,
+ * SCM-Manager will not start up.
+ *
+ * @param namespaceUpdateContext A context providing specifics about the namespace, whose data should be migrated
+ * (eg. its name).
+ */
+ @SuppressWarnings("java:S112") // we suppress this one, because an implementation should feel free to throw any exception it deems necessary
+ void doUpdate(NamespaceUpdateContext namespaceUpdateContext) throws Exception;
+}
diff --git a/scm-core/src/main/java/sonia/scm/update/NamespaceUpdateIterator.java b/scm-core/src/main/java/sonia/scm/update/NamespaceUpdateIterator.java
new file mode 100644
index 0000000000..f93cd5c9f6
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/update/NamespaceUpdateIterator.java
@@ -0,0 +1,75 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.update;
+
+import sonia.scm.migration.UpdateException;
+
+import java.util.function.Consumer;
+
+/**
+ * Implementations of this interface can be used to iterate all namespaces in update steps.
+ *
+ * @since 2.47.0
+ */
+public interface NamespaceUpdateIterator {
+
+ /**
+ * Calls the given consumer with each namespace.
+ *
+ * @since 2.47.0
+ */
+ void forEachNamespace(Consumer namespace);
+
+ /**
+ * Equivalent to {@link #forEachNamespace(Consumer)} with the difference, that you can throw exceptions in the given
+ * update code, that will then be wrapped in a {@link UpdateException}.
+ *
+ * @since 2.47.0
+ */
+ default void updateEachNamespace(Updater updater) {
+ forEachNamespace(
+ namespace -> {
+ try {
+ updater.update(namespace);
+ } catch (Exception e) {
+ throw new UpdateException("failed to update namespace " + namespace, e);
+ }
+ }
+ );
+ }
+
+ /**
+ * Simple callback with the name of an existing namespace with the possibility to throw exceptions.
+ *
+ * @since 2.47.0
+ */
+ interface Updater {
+ /**
+ * Implements the update logic for a single namespace, denoted by its name.
+ */
+ @SuppressWarnings("java:S112") // We explicitly want to allow arbitrary exceptions here
+ void update(String namespace) throws Exception;
+ }
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileNamespaceUpdateIterator.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileNamespaceUpdateIterator.java
new file mode 100644
index 0000000000..366d0d1b0b
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileNamespaceUpdateIterator.java
@@ -0,0 +1,70 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import sonia.scm.migration.UpdateException;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryLocationResolver;
+import sonia.scm.update.NamespaceUpdateIterator;
+
+import javax.inject.Inject;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.function.Consumer;
+
+public class FileNamespaceUpdateIterator implements NamespaceUpdateIterator {
+
+ private final RepositoryLocationResolver locationResolver;
+ private final JAXBContext jaxbContext;
+
+ @Inject
+ public FileNamespaceUpdateIterator(RepositoryLocationResolver locationResolver) {
+ this.locationResolver = locationResolver;
+ try {
+ jaxbContext = JAXBContext.newInstance(Repository.class);
+ } catch (JAXBException ex) {
+ throw new IllegalStateException("failed to create jaxb context for repository", ex);
+ }
+ }
+
+ @Override
+ public void forEachNamespace(Consumer namespaceConsumer) {
+ Collection namespaces = new HashSet<>();
+ locationResolver
+ .forClass(Path.class)
+ .forAllLocations((repositoryId, path) -> {
+ try {
+ Repository metadata = (Repository) jaxbContext.createUnmarshaller().unmarshal(path.resolve("metadata.xml").toFile());
+ namespaces.add(metadata.getNamespace());
+ } catch (JAXBException e) {
+ throw new UpdateException("Failed to read metadata for repository " + repositoryId, e);
+ }
+ });
+ namespaces.forEach(namespaceConsumer);
+ }
+}
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/FileNamespaceUpdateIteratorTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/FileNamespaceUpdateIteratorTest.java
new file mode 100644
index 0000000000..5b66e4cafb
--- /dev/null
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/FileNamespaceUpdateIteratorTest.java
@@ -0,0 +1,68 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.store;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.TempDirRepositoryLocationResolver;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class FileNamespaceUpdateIteratorTest {
+
+ private TempDirRepositoryLocationResolver locationResolver;
+
+ @BeforeEach
+ void initLocationResolver(@TempDir Path tempDir) throws IOException {
+ locationResolver = new TempDirRepositoryLocationResolver(tempDir.toFile());
+ Files.write(tempDir.resolve("metadata.xml"), asList(
+ "",
+ " hitchhike",
+ ""
+ ));
+ }
+
+ @Test
+ void shouldFindNamespaces() {
+ Collection foundNamespaces = new ArrayList<>();
+ new FileNamespaceUpdateIterator(locationResolver)
+ .forEachNamespace(foundNamespaces::add);
+
+ assertThat(foundNamespaces).containsExactly("hitchhike");
+ }
+}
diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStore.java b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStore.java
index 20fa3d664f..3f748243bf 100644
--- a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStore.java
+++ b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStore.java
@@ -37,7 +37,7 @@ import java.util.Map;
public class InMemoryByteConfigurationEntryStore implements ConfigurationEntryStore {
- private final Class type;
+ private Class type;
private final KeyGenerator generator = new UUIDKeyGenerator();
private final Map store = new HashMap<>();
@@ -104,4 +104,8 @@ public class InMemoryByteConfigurationEntryStore implements ConfigurationEntr
}
return null;
}
+
+ void overrideType(Class type) {
+ this.type = type;
+ }
}
diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStoreFactory.java
index eeec98ec13..92ffaa97d2 100644
--- a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStoreFactory.java
+++ b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationEntryStoreFactory.java
@@ -51,6 +51,8 @@ public class InMemoryByteConfigurationEntryStoreFactory implements Configuration
@SuppressWarnings("unchecked")
public ConfigurationEntryStore get(Class type, String name) {
- return stores.computeIfAbsent(name, n -> new InMemoryByteConfigurationEntryStore<>(type));
+ InMemoryByteConfigurationEntryStore store = stores.computeIfAbsent(name, n -> new InMemoryByteConfigurationEntryStore<>(type));
+ store.overrideType(type);
+ return store;
}
}
diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStore.java b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStore.java
index c7c75ace57..a8bdc44b96 100644
--- a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStore.java
+++ b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStore.java
@@ -34,7 +34,7 @@ import java.nio.charset.StandardCharsets;
*/
public class InMemoryByteConfigurationStore implements ConfigurationStore {
- private final Class type;
+ private Class type;
byte[] store;
public InMemoryByteConfigurationStore(Class type) {
@@ -73,4 +73,8 @@ public class InMemoryByteConfigurationStore implements ConfigurationStore
public void setRawXml(String xml) {
store = xml.getBytes(StandardCharsets.UTF_8);
}
+
+ void overrideType(Class type) {
+ this.type = type;
+ }
}
diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStoreFactory.java
index 4c6ffb6934..f63d4675a6 100644
--- a/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStoreFactory.java
+++ b/scm-test/src/main/java/sonia/scm/store/InMemoryByteConfigurationStoreFactory.java
@@ -46,6 +46,8 @@ public class InMemoryByteConfigurationStoreFactory implements ConfigurationStore
@SuppressWarnings("unchecked")
public ConfigurationStore getStore(Class type, String name) {
- return stores.computeIfAbsent(name, n -> new InMemoryByteConfigurationStore<>(type));
+ InMemoryByteConfigurationStore store = stores.computeIfAbsent(name, n -> new InMemoryByteConfigurationStore<>(type));
+ store.overrideType(type);
+ return store;
}
}
diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStore.java b/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStore.java
index 46b1a1623a..1736b1d0d4 100644
--- a/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStore.java
+++ b/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStore.java
@@ -37,7 +37,7 @@ import java.util.Map;
public class InMemoryByteDataStore implements DataStore {
- private final Class type;
+ private Class type;
private final KeyGenerator generator = new UUIDKeyGenerator();
private final Map store = new HashMap<>();
@@ -104,4 +104,8 @@ public class InMemoryByteDataStore implements DataStore {
}
return null;
}
+
+ void overrideType(Class type) {
+ this.type = type;
+ }
}
diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStoreFactory.java
index 348255b609..05997ae87d 100644
--- a/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStoreFactory.java
+++ b/scm-test/src/main/java/sonia/scm/store/InMemoryByteDataStoreFactory.java
@@ -47,6 +47,8 @@ public class InMemoryByteDataStoreFactory implements DataStoreFactory, InMemoryS
@SuppressWarnings("unchecked")
public DataStore getStore(Class type, String name) {
- return stores.computeIfAbsent(name, n -> new InMemoryByteDataStore<>(type));
+ InMemoryByteDataStore store = stores.computeIfAbsent(name, n -> new InMemoryByteDataStore<>(type));
+ store.overrideType(type);
+ return store;
}
}
diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java
index 07fe9d2443..f1b87f2253 100644
--- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java
+++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java
@@ -61,6 +61,7 @@ import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.DataStoreFactory;
import sonia.scm.store.DefaultBlobDirectoryAccess;
import sonia.scm.store.FileBlobStoreFactory;
+import sonia.scm.store.FileNamespaceUpdateIterator;
import sonia.scm.store.FileRepositoryUpdateIterator;
import sonia.scm.store.FileStoreUpdateStepUtilFactory;
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
@@ -68,6 +69,7 @@ import sonia.scm.store.JAXBConfigurationStoreFactory;
import sonia.scm.store.JAXBDataStoreFactory;
import sonia.scm.store.JAXBPropertyFileAccess;
import sonia.scm.update.BlobDirectoryAccess;
+import sonia.scm.update.NamespaceUpdateIterator;
import sonia.scm.update.PropertyFileAccess;
import sonia.scm.update.RepositoryUpdateIterator;
import sonia.scm.update.StoreUpdateStepUtilFactory;
@@ -124,6 +126,7 @@ public class BootstrapModule extends AbstractModule {
bind(PropertyFileAccess.class, JAXBPropertyFileAccess.class);
bind(BlobDirectoryAccess.class, DefaultBlobDirectoryAccess.class);
bind(RepositoryUpdateIterator.class, FileRepositoryUpdateIterator.class);
+ bind(NamespaceUpdateIterator.class, FileNamespaceUpdateIterator.class);
bind(StoreUpdateStepUtilFactory.class, FileStoreUpdateStepUtilFactory.class);
bind(new TypeLiteral>() {}).to(new TypeLiteral() {});
diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/UpdateStepModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/UpdateStepModule.java
index 9a5415ea03..77a8c3c427 100644
--- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/UpdateStepModule.java
+++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/UpdateStepModule.java
@@ -26,6 +26,7 @@ package sonia.scm.lifecycle.modules;
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.Multibinder;
+import sonia.scm.migration.NamespaceUpdateStep;
import sonia.scm.migration.RepositoryUpdateStep;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.PluginLoader;
@@ -41,7 +42,8 @@ public class UpdateStepModule extends AbstractModule {
@Override
protected void configure() {
Multibinder updateStepBinder = Multibinder.newSetBinder(binder(), UpdateStep.class);
- Multibinder repositoryUdateStepBinder = Multibinder.newSetBinder(binder(), RepositoryUpdateStep.class);
+ Multibinder repositoryUpdateStepBinder = Multibinder.newSetBinder(binder(), RepositoryUpdateStep.class);
+ Multibinder namespaceUpdateStepBinder = Multibinder.newSetBinder(binder(), NamespaceUpdateStep.class);
pluginLoader
.getExtensionProcessor()
.byExtensionPoint(UpdateStep.class)
@@ -49,6 +51,10 @@ public class UpdateStepModule extends AbstractModule {
pluginLoader
.getExtensionProcessor()
.byExtensionPoint(RepositoryUpdateStep.class)
- .forEach(stepClass -> repositoryUdateStepBinder.addBinding().to(stepClass));
+ .forEach(stepClass -> repositoryUpdateStepBinder.addBinding().to(stepClass));
+ pluginLoader
+ .getExtensionProcessor()
+ .byExtensionPoint(NamespaceUpdateStep.class)
+ .forEach(stepClass -> namespaceUpdateStepBinder.addBinding().to(stepClass));
}
}
diff --git a/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java b/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java
index 8adda1ff26..dff597698a 100644
--- a/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java
+++ b/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java
@@ -26,6 +26,8 @@ package sonia.scm.update;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import sonia.scm.migration.NamespaceUpdateContext;
+import sonia.scm.migration.NamespaceUpdateStep;
import sonia.scm.migration.RepositoryUpdateContext;
import sonia.scm.migration.RepositoryUpdateStep;
import sonia.scm.migration.UpdateException;
@@ -46,28 +48,33 @@ public class UpdateEngine {
public static final Logger LOG = LoggerFactory.getLogger(UpdateEngine.class);
-
private final List steps;
private final RepositoryUpdateIterator repositoryUpdateIterator;
+ private final NamespaceUpdateIterator namespaceUpdateIterator;
private final UpdateStepStore updateStepStore;
@Inject
public UpdateEngine(
Set globalSteps,
Set repositorySteps,
+ Set namespaceSteps,
RepositoryUpdateIterator repositoryUpdateIterator,
- UpdateStepStore updateStepStore) {
+ NamespaceUpdateIterator namespaceUpdateIterator, UpdateStepStore updateStepStore) {
this.repositoryUpdateIterator = repositoryUpdateIterator;
+ this.namespaceUpdateIterator = namespaceUpdateIterator;
this.updateStepStore = updateStepStore;
- this.steps = sortSteps(globalSteps, repositorySteps);
+ this.steps = sortSteps(globalSteps, repositorySteps, namespaceSteps);
}
- private List sortSteps(Set globalSteps, Set repositorySteps) {
+ private List sortSteps(Set globalSteps, Set repositorySteps, Set namespaceSteps) {
LOG.trace("sorting available update steps:");
List sortedSteps =
concat(
- globalSteps.stream().filter(this::notRunYet).map(GlobalUpdateStepWrapper::new),
- repositorySteps.stream().map(RepositoryUpdateStepWrapper::new))
+ concat(
+ globalSteps.stream().filter(this::notRunYet).map(GlobalUpdateStepWrapper::new),
+ repositorySteps.stream().map(RepositoryUpdateStepWrapper::new)),
+ namespaceSteps.stream().map(NamespaceUpdateStepWrapper::new)
+ )
.sorted(
Comparator
.comparing(UpdateStepWrapper::getTargetVersion)
@@ -248,4 +255,52 @@ public class UpdateEngine {
return updateStepStore.notRunYet(repositoryId, delegate);
}
}
+
+ private class NamespaceUpdateStepWrapper extends UpdateStepWrapper {
+
+ private final NamespaceUpdateStep delegate;
+
+ public NamespaceUpdateStepWrapper(NamespaceUpdateStep delegate) {
+ super(delegate);
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean isGlobalUpdateStep() {
+ return false;
+ }
+
+ @Override
+ boolean isCoreUpdate() {
+ return false;
+ }
+
+ @Override
+ void doUpdate() {
+ namespaceUpdateIterator.updateEachNamespace(this::doUpdate);
+ }
+
+ @Override
+ void doUpdate(String namespace) throws Exception {
+ if (notRunYet(namespace)) {
+ LOG.info("running update step for type {} and version {} (class {}) for namespace {}",
+ delegate.getAffectedDataType(),
+ delegate.getTargetVersion(),
+ delegate.getClass().getName(),
+ namespace
+ );
+ delegate.doUpdate(new NamespaceUpdateContext(namespace));
+ updateStepStore.storeExecutedUpdate(namespace, delegate);
+ }
+ }
+
+ private boolean notRunYet(String namespace) {
+ LOG.trace("checking whether to run update step for type {} and version {} on namespace {}",
+ delegate.getAffectedDataType(),
+ delegate.getTargetVersion(),
+ namespace
+ );
+ return updateStepStore.notRunYet(namespace, delegate);
+ }
+ }
}
diff --git a/scm-webapp/src/main/java/sonia/scm/update/UpdateStepStore.java b/scm-webapp/src/main/java/sonia/scm/update/UpdateStepStore.java
index a44354912b..c2146b42f3 100644
--- a/scm-webapp/src/main/java/sonia/scm/update/UpdateStepStore.java
+++ b/scm-webapp/src/main/java/sonia/scm/update/UpdateStepStore.java
@@ -26,6 +26,7 @@ package sonia.scm.update;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import sonia.scm.migration.NamespaceUpdateStep;
import sonia.scm.migration.RepositoryUpdateStep;
import sonia.scm.migration.UpdateStep;
import sonia.scm.migration.UpdateStepTarget;
@@ -52,6 +53,11 @@ class UpdateStepStore {
storeNewVersion(store, updateStep);
}
+ void storeExecutedUpdate(String namespace, NamespaceUpdateStep updateStep) {
+ ConfigurationEntryStore store = createNamespaceStore(namespace);
+ storeNewVersion(store, updateStep);
+ }
+
void storeExecutedUpdate(UpdateStep updateStep) {
ConfigurationEntryStore store = createGlobalStore();
storeNewVersion(store, updateStep);
@@ -65,6 +71,10 @@ class UpdateStepStore {
return notRunYet(createRepositoryStore(repositoryId), updateStep);
}
+ boolean notRunYet(String namespace, NamespaceUpdateStep updateStep) {
+ return notRunYet(createNamespaceStore(namespace), updateStep);
+ }
+
private void storeNewVersion(ConfigurationEntryStore store, UpdateStepTarget updateStep) {
UpdateVersionInfo newVersionInfo = new UpdateVersionInfo(updateStep.getTargetVersion().getParsedVersion());
store.put(updateStep.getAffectedDataType(), newVersionInfo);
@@ -99,4 +109,12 @@ class UpdateStepStore {
.forRepository(repositoryId)
.build();
}
+
+ private ConfigurationEntryStore createNamespaceStore(String namespace) {
+ return storeFactory
+ .withType(UpdateVersionInfo.class)
+ .withName(STORE_NAME)
+ .forNamespace(namespace)
+ .build();
+ }
}
diff --git a/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java b/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java
index 5a8f007ac0..d4cd9ce3f6 100644
--- a/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java
@@ -26,6 +26,8 @@ package sonia.scm.update;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import sonia.scm.migration.NamespaceUpdateContext;
+import sonia.scm.migration.NamespaceUpdateStep;
import sonia.scm.migration.RepositoryUpdateContext;
import sonia.scm.migration.RepositoryUpdateStep;
import sonia.scm.migration.UpdateStep;
@@ -52,6 +54,7 @@ class UpdateEngineTest {
ConfigurationEntryStoreFactory storeFactory = new InMemoryByteConfigurationEntryStoreFactory();
RepositoryUpdateIterator repositoryUpdateIterator = mock(RepositoryUpdateIterator.class, CALLS_REAL_METHODS);
+ NamespaceUpdateIterator namespaceUpdateIterator = mock(NamespaceUpdateIterator.class, CALLS_REAL_METHODS);
UpdateStepStore updateStepStore = new UpdateStepStore(storeFactory);
List processedUpdates = new ArrayList<>();
@@ -66,6 +69,16 @@ class UpdateEngineTest {
}).when(repositoryUpdateIterator).forEachRepository(any());
}
+ @BeforeEach
+ void mockNamespaces() {
+ doAnswer(invocation -> {
+ Consumer consumer = invocation.getArgument(0, Consumer.class);
+ consumer.accept("hitchhikers");
+ consumer.accept("vogons");
+ return null;
+ }).when(namespaceUpdateIterator).forEachNamespace(any());
+ }
+
@Test
void shouldProcessStepsInCorrectOrder() {
LinkedHashSet updateSteps = new LinkedHashSet<>();
@@ -74,7 +87,7 @@ class UpdateEngineTest {
updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
updateSteps.add(new FixedVersionUpdateStep("test", "1.1.0"));
- UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), repositoryUpdateIterator, updateStepStore);
+ UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore);
updateEngine.update();
assertThat(processedUpdates)
@@ -90,7 +103,7 @@ class UpdateEngineTest {
updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.0"));
- UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, repositoryUpdateIterator, updateStepStore);
+ UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore);
updateEngine.update();
assertThat(processedUpdates)
@@ -106,13 +119,29 @@ class UpdateEngineTest {
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.0"));
- UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, repositoryUpdateIterator, updateStepStore);
+ UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore);
updateEngine.update("1337");
assertThat(processedUpdates)
.containsExactly("test:1.1.0-1337", "test:1.1.1-1337");
}
+ @Test
+ void shouldProcessStepsForSingleNamespace() {
+ LinkedHashSet updateSteps = new LinkedHashSet<>();
+ LinkedHashSet namespaceUpdateSteps = new LinkedHashSet<>();
+
+ updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
+ namespaceUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
+ namespaceUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.0"));
+
+ UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), namespaceUpdateSteps, repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore);
+ updateEngine.update("hitchhikers");
+
+ assertThat(processedUpdates)
+ .containsExactly("test:1.1.0/hitchhikers", "test:1.1.1/hitchhikers");
+ }
+
@Test
void shouldProcessCoreStepsBeforeOther() {
LinkedHashSet updateSteps = new LinkedHashSet<>();
@@ -120,7 +149,7 @@ class UpdateEngineTest {
updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
updateSteps.add(new CoreFixedVersionUpdateStep("core", "1.2.0"));
- UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), repositoryUpdateIterator, updateStepStore);
+ UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore);
updateEngine.update();
assertThat(processedUpdates)
@@ -132,7 +161,7 @@ class UpdateEngineTest {
Set updateSteps = singleton(new FixedVersionUpdateStep("test", "1.2.0"));
Set repositoryUpdateSteps = singleton(new FixedVersionUpdateStep("test", "1.2.0"));
- UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, repositoryUpdateIterator, updateStepStore);
+ UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore);
updateEngine.update();
assertThat(processedUpdates)
@@ -145,12 +174,12 @@ class UpdateEngineTest {
updateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
- UpdateEngine firstUpdateEngine = new UpdateEngine(updateSteps, emptySet(), repositoryUpdateIterator, updateStepStore);
+ UpdateEngine firstUpdateEngine = new UpdateEngine(updateSteps, emptySet(), emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore);
firstUpdateEngine.update();
processedUpdates.clear();
- UpdateEngine secondUpdateEngine = new UpdateEngine(updateSteps, emptySet(), repositoryUpdateIterator, updateStepStore);
+ UpdateEngine secondUpdateEngine = new UpdateEngine(updateSteps, emptySet(), emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore);
secondUpdateEngine.update();
assertThat(processedUpdates).isEmpty();
@@ -162,39 +191,58 @@ class UpdateEngineTest {
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
- UpdateEngine firstUpdateEngine = new UpdateEngine(emptySet(), repositoryUpdateSteps, repositoryUpdateIterator, updateStepStore);
+ UpdateEngine firstUpdateEngine = new UpdateEngine(emptySet(), repositoryUpdateSteps, emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore);
firstUpdateEngine.update();
processedUpdates.clear();
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
- UpdateEngine secondUpdateEngine = new UpdateEngine(emptySet(), repositoryUpdateSteps, repositoryUpdateIterator, updateStepStore);
+ UpdateEngine secondUpdateEngine = new UpdateEngine(emptySet(), repositoryUpdateSteps, emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore);
secondUpdateEngine.update();
assertThat(processedUpdates)
.containsExactly("test:1.2.0-42", "test:1.2.0-1337");
}
+ @Test
+ void shouldRunNamespaceStepsOnlyOnce() {
+ LinkedHashSet namespaceUpdateSteps = new LinkedHashSet<>();
+
+ namespaceUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
+
+ UpdateEngine firstUpdateEngine = new UpdateEngine(emptySet(), emptySet(), namespaceUpdateSteps, repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore);
+ firstUpdateEngine.update();
+
+ processedUpdates.clear();
+
+ namespaceUpdateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
+ UpdateEngine secondUpdateEngine = new UpdateEngine(emptySet(), emptySet(), namespaceUpdateSteps, repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore);
+ secondUpdateEngine.update();
+
+ assertThat(processedUpdates)
+ .containsExactly("test:1.2.0/hitchhikers", "test:1.2.0/vogons");
+ }
+
@Test
void shouldRunStepsForDifferentTypesIndependently() {
LinkedHashSet updateSteps = new LinkedHashSet<>();
updateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
- UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), repositoryUpdateIterator, updateStepStore);
+ UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore);
updateEngine.update();
processedUpdates.clear();
updateSteps.add(new FixedVersionUpdateStep("other", "1.1.1"));
- updateEngine = new UpdateEngine(updateSteps, emptySet(), repositoryUpdateIterator, updateStepStore);
+ updateEngine = new UpdateEngine(updateSteps, emptySet(), emptySet(), repositoryUpdateIterator, namespaceUpdateIterator, updateStepStore);
updateEngine.update();
assertThat(processedUpdates).containsExactly("other:1.1.1");
}
- class FixedVersionUpdateStep implements UpdateStep, RepositoryUpdateStep {
+ class FixedVersionUpdateStep implements UpdateStep, RepositoryUpdateStep, NamespaceUpdateStep {
private final String type;
private final String version;
@@ -222,6 +270,11 @@ class UpdateEngineTest {
public void doUpdate(RepositoryUpdateContext repositoryUpdateContext) throws Exception {
processedUpdates.add(type + ":" + version + "-" + repositoryUpdateContext.getRepositoryId());
}
+
+ @Override
+ public void doUpdate(NamespaceUpdateContext namespaceUpdateContext) throws Exception {
+ processedUpdates.add(type + ":" + version + "/" + namespaceUpdateContext.getNamespace());
+ }
}
class CoreFixedVersionUpdateStep extends FixedVersionUpdateStep implements CoreUpdateStep {