diff --git a/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java index cf58fc43c7..4cc124c5d7 100644 --- a/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java +++ b/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java @@ -82,7 +82,7 @@ public interface BlobStoreFactory { final class FloatingStoreParameters implements StoreParameters { private String name; - private Repository repository; + private String repositoryId; private final BlobStoreFactory factory; @@ -96,8 +96,8 @@ final class FloatingStoreParameters implements StoreParameters { } @Override - public Repository getRepository() { - return repository; + public String getRepositoryId() { + return repositoryId; } public class Builder { @@ -113,7 +113,18 @@ final class FloatingStoreParameters implements StoreParameters { * @return Floating API to finish the call. */ public FloatingStoreParameters.Builder forRepository(Repository repository) { - FloatingStoreParameters.this.repository = repository; + FloatingStoreParameters.this.repositoryId = repository.getId(); + return this; + } + + /** + * Use this to create or get a {@link BlobStore} for a specific repository. This step is optional. If you want to + * have a global {@link BlobStore}, omit this. + * @param repositoryId The id of the optional repository for the {@link BlobStore}. + * @return Floating API to finish the call. + */ + public FloatingStoreParameters.Builder forRepository(String repositoryId) { + FloatingStoreParameters.this.repositoryId = repositoryId; return this; } diff --git a/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java index 80f9cb3df9..7ff1633dc3 100644 --- a/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java +++ b/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java @@ -120,7 +120,18 @@ final class TypedFloatingConfigurationEntryStoreParameters { * @return Floating API to finish the call. */ public OptionalRepositoryBuilder forRepository(Repository repository) { - parameters.setRepository(repository); + parameters.setRepositoryId(repository.getId()); + return this; + } + + /** + * Use this to create or get a {@link ConfigurationEntryStore} for a specific repository. This step is optional. If + * you want to have a global {@link ConfigurationEntryStore}, omit this. + * @param repositoryId The id of the optional repository for the {@link ConfigurationEntryStore}. + * @return Floating API to finish the call. + */ + public OptionalRepositoryBuilder forRepository(String repositoryId) { + parameters.setRepositoryId(repositoryId); return this; } diff --git a/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java index 6624f307e7..cef39a303b 100644 --- a/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java +++ b/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java @@ -120,7 +120,18 @@ final class TypedFloatingConfigurationStoreParameters { * @return Floating API to finish the call. */ public OptionalRepositoryBuilder forRepository(Repository repository) { - parameters.setRepository(repository); + parameters.setRepositoryId(repository.getId()); + return this; + } + + /** + * Use this to create or get a {@link ConfigurationStore} for a specific repository. This step is optional. If you + * want to have a global {@link ConfigurationStore}, omit this. + * @param repositoryId The id of the optional repository for the {@link ConfigurationStore}. + * @return Floating API to finish the call. + */ + public OptionalRepositoryBuilder forRepository(String repositoryId) { + parameters.setRepositoryId(repositoryId); return this; } diff --git a/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java index 564c339d3d..cc149d46ce 100644 --- a/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java +++ b/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java @@ -117,7 +117,18 @@ final class TypedFloatingDataStoreParameters { * @return Floating API to finish the call. */ public OptionalRepositoryBuilder forRepository(Repository repository) { - parameters.setRepository(repository); + parameters.setRepositoryId(repository.getId()); + return this; + } + + /** + * Use this to create or get a {@link DataStore} for a specific repository. This step is optional. If you + * want to have a global {@link DataStore}, omit this. + * @param repositoryId The id of the optional repository for the {@link DataStore}. + * @return Floating API to finish the call. + */ + public OptionalRepositoryBuilder forRepository(String repositoryId) { + parameters.setRepositoryId(repositoryId); return this; } diff --git a/scm-core/src/main/java/sonia/scm/store/StoreParameters.java b/scm-core/src/main/java/sonia/scm/store/StoreParameters.java index da8ee4c916..38404398cc 100644 --- a/scm-core/src/main/java/sonia/scm/store/StoreParameters.java +++ b/scm-core/src/main/java/sonia/scm/store/StoreParameters.java @@ -12,5 +12,5 @@ public interface StoreParameters { String getName(); - Repository getRepository(); + String getRepositoryId(); } diff --git a/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java b/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java index 116bccac41..060a9706f8 100644 --- a/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java +++ b/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java @@ -15,5 +15,5 @@ public interface TypedStoreParameters { String getName(); - Repository getRepository(); + String getRepositoryId(); } diff --git a/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersImpl.java b/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersImpl.java index 50ce6a496b..3abf1d2192 100644 --- a/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersImpl.java +++ b/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersImpl.java @@ -1,11 +1,9 @@ package sonia.scm.store; -import sonia.scm.repository.Repository; - class TypedStoreParametersImpl implements TypedStoreParameters { private Class type; private String name; - private Repository repository; + private String repositoryId; @Override public Class getType() { @@ -26,11 +24,11 @@ class TypedStoreParametersImpl implements TypedStoreParameters { } @Override - public Repository getRepository() { - return repository; + public String getRepositoryId() { + return repositoryId; } - void setRepository(Repository repository) { - this.repository = repository; + void setRepositoryId(String repositoryId) { + this.repositoryId = repositoryId; } } diff --git a/scm-core/src/main/java/sonia/scm/update/GroupV1PropertyReader.java b/scm-core/src/main/java/sonia/scm/update/GroupV1PropertyReader.java new file mode 100644 index 0000000000..a7703c1b76 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/GroupV1PropertyReader.java @@ -0,0 +1,15 @@ +package sonia.scm.update; + +import java.util.Map; + +public class GroupV1PropertyReader implements V1PropertyReader { + @Override + public String getStoreName() { + return "group-properties-v1"; + } + + @Override + public Instance createInstance(Map all) { + return new MapBasedPropertyReaderInstance(all); + } +} diff --git a/scm-core/src/main/java/sonia/scm/update/MapBasedPropertyReaderInstance.java b/scm-core/src/main/java/sonia/scm/update/MapBasedPropertyReaderInstance.java new file mode 100644 index 0000000000..65e6a71958 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/MapBasedPropertyReaderInstance.java @@ -0,0 +1,36 @@ +package sonia.scm.update; + +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.stream.Stream; + +public class MapBasedPropertyReaderInstance implements V1PropertyReader.Instance { + private final Stream> all; + + public MapBasedPropertyReaderInstance(Map all) { + this(all.entrySet().stream()); + } + + private MapBasedPropertyReaderInstance(Stream> all) { + this.all = all; + } + + @Override + public void forEachEntry(BiConsumer propertiesForNameConsumer) { + all.forEach(e -> call(e.getKey(), e.getValue(), propertiesForNameConsumer)); + } + + @Override + public V1PropertyReader.Instance havingAnyOf(String... keys) { + return new MapBasedPropertyReaderInstance(all.filter(e -> e.getValue().hasAny(keys))); + } + + @Override + public V1PropertyReader.Instance havingAllOf(String... keys) { + return new MapBasedPropertyReaderInstance(all.filter(e -> e.getValue().hasAll(keys))); + } + + private void call(String repositoryId, V1Properties properties, BiConsumer propertiesForNameConsumer) { + propertiesForNameConsumer.accept(repositoryId, properties); + } +} diff --git a/scm-core/src/main/java/sonia/scm/update/PropertyFileAccess.java b/scm-core/src/main/java/sonia/scm/update/PropertyFileAccess.java new file mode 100644 index 0000000000..726d589d02 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/PropertyFileAccess.java @@ -0,0 +1,53 @@ +package sonia.scm.update; + +import java.io.IOException; +import java.nio.file.Path; + +public interface PropertyFileAccess { + + /** + * Use this to rename a configuration file. + * @param oldName The old file name. + * @return Object to specify the new file name. + */ + Target renameGlobalConfigurationFrom(String oldName); + + interface Target { + /** + * Renames a file to the new name given here. + * @param newName The new file name. + * @throws IOException If the file could not be renamed. + */ + void to(String newName) throws IOException; + } + + /** + * Creates a tool object for store migration from v1 to v2. + * @param name The name of the store to be handled. + * @return The tool object for the named store. + */ + StoreFileTools forStoreName(String name); + + interface StoreFileTools { + /** + * Iterates over all found store files (that is, files ending with ".xml") and calls the + * given consumer for each file, giving the file and the name of the data file. + * @param storeFileConsumer This consumer will be called for each file found. + * @throws IOException May be thrown when an exception occurs. + */ + void forStoreFiles(FileConsumer storeFileConsumer) throws IOException; + + /** + * Moves a data file to the new location for a repository with the given id. If there + * is no directory for the given repository id, nothing will be done. + * @param storeFile The name of the store file. + * @param repositoryId The id of the repository as the new target for the store file. + * @throws IOException When the file could not be moved. + */ + void moveAsRepositoryStore(Path storeFile, String repositoryId) throws IOException; + } + + interface FileConsumer { + void accept(Path file, String storeName) throws IOException; + } +} diff --git a/scm-core/src/main/java/sonia/scm/update/RepositoryV1PropertyReader.java b/scm-core/src/main/java/sonia/scm/update/RepositoryV1PropertyReader.java new file mode 100644 index 0000000000..5faf4500cd --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/RepositoryV1PropertyReader.java @@ -0,0 +1,16 @@ +package sonia.scm.update; + +import java.util.Map; + +public class RepositoryV1PropertyReader implements V1PropertyReader { + @Override + public String getStoreName() { + return "repository-properties-v1"; + } + + @Override + public Instance createInstance(Map all) { + return new MapBasedPropertyReaderInstance(all); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/update/UserV1PropertyReader.java b/scm-core/src/main/java/sonia/scm/update/UserV1PropertyReader.java new file mode 100644 index 0000000000..b29c311d1c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/UserV1PropertyReader.java @@ -0,0 +1,15 @@ +package sonia.scm.update; + +import java.util.Map; + +public class UserV1PropertyReader implements V1PropertyReader { + @Override + public String getStoreName() { + return "user-properties-v1"; + } + + @Override + public Instance createInstance(Map all) { + return new MapBasedPropertyReaderInstance(all); + } +} diff --git a/scm-core/src/main/java/sonia/scm/update/V1Properties.java b/scm-core/src/main/java/sonia/scm/update/V1Properties.java new file mode 100644 index 0000000000..7df65efdb6 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/V1Properties.java @@ -0,0 +1,59 @@ +package sonia.scm.update; + +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; +import java.util.Optional; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static java.util.stream.Stream.empty; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "properties") +public class V1Properties { + @XmlElement(name = "item") + private List properties; + + public V1Properties() { + } + + public V1Properties(V1Property... properties) { + this(asList(properties)); + } + + public V1Properties(List properties) { + this.properties = properties; + } + + public String get(String key) { + return getOptional(key).orElse(null); + } + + public Optional getOptional(String key) { + return streamProps().filter(p -> key.equals(p.getKey())).map(V1Property::getValue).findFirst(); + } + + public Optional getBoolean(String key) { + return getOptional(key).map(Boolean::valueOf); + } + + public > Optional getEnum(String key, Class enumType) { + return getOptional(key).map(name -> Enum.valueOf(enumType, name)); + } + + public boolean hasAny(String[] keys) { + return streamProps().anyMatch(p -> stream(keys).anyMatch(k -> k.equals(p.getKey()))); + } + + public boolean hasAll(String[] keys) { + return stream(keys).allMatch(k -> streamProps().anyMatch(p -> k.equals(p.getKey()))); + } + + private Stream streamProps() { + return properties == null? empty(): properties.stream(); + } +} diff --git a/scm-core/src/main/java/sonia/scm/update/V1Property.java b/scm-core/src/main/java/sonia/scm/update/V1Property.java new file mode 100644 index 0000000000..c3dcf6a590 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/V1Property.java @@ -0,0 +1,49 @@ +package sonia.scm.update; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import java.util.Objects; + +@XmlAccessorType(XmlAccessType.FIELD) +public class V1Property { + private String key; + private String value; + + public V1Property() { + } + + public V1Property(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + V1Property that = (V1Property) o; + return Objects.equals(key, that.key) && + Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value); + } + + @Override + public String toString() { + return "V1Property{" + + "key='" + key + '\'' + + ", value='" + value + '\'' + + '}'; + } +} diff --git a/scm-core/src/main/java/sonia/scm/update/V1PropertyDAO.java b/scm-core/src/main/java/sonia/scm/update/V1PropertyDAO.java new file mode 100644 index 0000000000..46b1773c57 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/V1PropertyDAO.java @@ -0,0 +1,14 @@ +package sonia.scm.update; + +/** + * Use this to access old properties from an instance of SCM-Manager v1. + */ +public interface V1PropertyDAO { + /** + * Creates an instance of a property reader to process old properties. + * @param reader The reader for the origin of the properties (for example + * {@link V1PropertyReader#REPOSITORY_PROPERTY_READER} for properties of repositories). + * @return The reader instance. + */ + V1PropertyReader.Instance getProperties(V1PropertyReader reader); +} diff --git a/scm-core/src/main/java/sonia/scm/update/V1PropertyReader.java b/scm-core/src/main/java/sonia/scm/update/V1PropertyReader.java new file mode 100644 index 0000000000..4e5cc55115 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/V1PropertyReader.java @@ -0,0 +1,35 @@ +package sonia.scm.update; + +import java.util.Map; +import java.util.function.BiConsumer; + +public interface V1PropertyReader { + + V1PropertyReader REPOSITORY_PROPERTY_READER = new RepositoryV1PropertyReader(); + V1PropertyReader USER_PROPERTY_READER = new UserV1PropertyReader(); + V1PropertyReader GROUP_PROPERTY_READER = new GroupV1PropertyReader(); + + String getStoreName(); + + Instance createInstance(Map all); + + interface Instance { + /** + * Will call the given consumer for each id of the corresponding entity with its list of + * properties converted from v1. + * For example for repositories this will call the consumer with the id of each repository + * that had properties attached in v1. + */ + void forEachEntry(BiConsumer propertiesForNameConsumer); + + /** + * Filters for entities only having at least one property with a given key name. + */ + Instance havingAnyOf(String... keys); + + /** + * Filters for entities only having properties for all given key name. + */ + Instance havingAllOf(String... keys); + } +} diff --git a/scm-core/src/test/java/sonia/scm/update/MapBasedPropertyReaderInstanceTest.java b/scm-core/src/test/java/sonia/scm/update/MapBasedPropertyReaderInstanceTest.java new file mode 100644 index 0000000000..f214203866 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/update/MapBasedPropertyReaderInstanceTest.java @@ -0,0 +1,61 @@ +package sonia.scm.update; + +import com.google.common.collect.ImmutableMap; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +class MapBasedPropertyReaderInstanceTest { + + Map executedCalls = new HashMap<>(); + + BiConsumer consumer = (key, properties) -> executedCalls.put(key, properties); + + MapBasedPropertyReaderInstance instance = new MapBasedPropertyReaderInstance( + ImmutableMap.of( + "o1", new V1Properties( + new V1Property("k1", "v1-1"), + new V1Property("k2", "v1-2"), + new V1Property("k3", "v1-3") + ), + "o2", new V1Properties( + new V1Property("k1", "v2-1"), + new V1Property("k2", "v2-2") + ), + "o3", new V1Properties( + new V1Property("k1", "v3-1") + ) + ) + ); + + @Test + void shouldCallBackForEachObjectIfNotFiltered() { + instance.forEachEntry(consumer); + + Assertions.assertThat(executedCalls).hasSize(3); + } + + @Test + void shouldCallBackOnlyObjectsHavingAtLeastOneOfGivenKey() { + instance.havingAnyOf("k2", "k3").forEachEntry(consumer); + + Assertions.assertThat(executedCalls).hasSize(2).containsKeys("o1", "o2"); + } + + @Test + void shouldCallBackOnlyObjectsHavingAllOfGivenKey() { + instance.havingAllOf("k2", "k3").forEachEntry(consumer); + + Assertions.assertThat(executedCalls).hasSize(1).containsKeys("o1"); + } + + @Test + void shouldCombineFilters() { + instance.havingAnyOf("k2", "k3").havingAllOf("k3").forEachEntry(consumer); + + Assertions.assertThat(executedCalls).hasSize(1).containsKeys("o1"); + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java index d31179c1c2..967aa8fb1c 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java @@ -35,7 +35,6 @@ package sonia.scm.store; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; -import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.util.IOUtil; @@ -66,18 +65,18 @@ public abstract class FileBasedStoreFactory { } protected File getStoreLocation(StoreParameters storeParameters) { - return getStoreLocation(storeParameters.getName(), null, storeParameters.getRepository()); + return getStoreLocation(storeParameters.getName(), null, storeParameters.getRepositoryId()); } protected File getStoreLocation(TypedStoreParameters storeParameters) { - return getStoreLocation(storeParameters.getName(), storeParameters.getType(), storeParameters.getRepository()); + return getStoreLocation(storeParameters.getName(), storeParameters.getType(), storeParameters.getRepositoryId()); } - protected File getStoreLocation(String name, Class type, Repository repository) { + protected File getStoreLocation(String name, Class type, String repositoryId) { File storeDirectory; - if (repository != null) { - LOG.debug("create store with type: {}, name: {} and repository: {}", type, name, repository.getNamespaceAndName()); - storeDirectory = this.getStoreDirectory(store, repository); + if (repositoryId != null) { + LOG.debug("create store with type: {}, name: {} and repository id: {}", type, name, repositoryId); + storeDirectory = this.getStoreDirectory(store, repositoryId); } else { LOG.debug("create store with type: {} and name: {} ", type, name); storeDirectory = this.getStoreDirectory(store); @@ -89,11 +88,11 @@ public abstract class FileBasedStoreFactory { /** * Get the store directory of a specific repository * @param store the type of the store - * @param repository the repo + * @param repositoryId the id of the repossitory * @return the store directory of a specific repository */ - private File getStoreDirectory(Store store, Repository repository) { - return new File(repositoryLocationResolver.forClass(Path.class).getLocation(repository.getId()).toFile(), store.getRepositoryStoreDirectory()); + private File getStoreDirectory(Store store, String repositoryId) { + return new File(repositoryLocationResolver.forClass(Path.class).getLocation(repositoryId).toFile(), store.getRepositoryStoreDirectory()); } /** diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java index 96403140ef..f66bccb7ba 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java @@ -59,7 +59,9 @@ public class JAXBConfigurationEntryStoreFactory extends FileBasedStoreFactory @Override public ConfigurationEntryStore getStore(TypedStoreParameters storeParameters) { - return new JAXBConfigurationEntryStore<>(getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), storeParameters.getType(), storeParameters.getRepository()), keyGenerator, storeParameters.getType()); + return new JAXBConfigurationEntryStore<>( + getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), storeParameters.getType(), storeParameters.getRepositoryId()), + keyGenerator, + storeParameters.getType()); } - } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java index bb68ab93dc..e66394cb5d 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java @@ -55,6 +55,10 @@ public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory impleme @Override public JAXBConfigurationStore getStore(TypedStoreParameters storeParameters) { - return new JAXBConfigurationStore<>(storeParameters.getType(), getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), storeParameters.getType(), storeParameters.getRepository())); + return new JAXBConfigurationStore<>( + storeParameters.getType(), + getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), + storeParameters.getType(), + storeParameters.getRepositoryId())); } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java new file mode 100644 index 0000000000..b8d30bc0c1 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java @@ -0,0 +1,91 @@ +package sonia.scm.store; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.update.PropertyFileAccess; +import sonia.scm.util.IOUtil; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +public class JAXBPropertyFileAccess implements PropertyFileAccess { + + private static final Logger LOG = LoggerFactory.getLogger(JAXBPropertyFileAccess.class); + + public static final String XML_FILENAME_SUFFIX = ".xml"; + private final SCMContextProvider contextProvider; + private final RepositoryLocationResolver locationResolver; + + @Inject + public JAXBPropertyFileAccess(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) { + this.contextProvider = contextProvider; + this.locationResolver = locationResolver; + } + + @Override + public Target renameGlobalConfigurationFrom(String oldName) { + return newName -> { + Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME); + Path oldConfigFile = configDir.resolve(oldName + XML_FILENAME_SUFFIX); + Path newConfigFile = configDir.resolve(newName + XML_FILENAME_SUFFIX); + Files.move(oldConfigFile, newConfigFile); + }; + } + + @Override + public StoreFileTools forStoreName(String storeName) { + return new StoreFileTools() { + @Override + public void forStoreFiles(FileConsumer storeFileConsumer) throws IOException { + Path v1storeDir = computeV1StoreDir(); + if (Files.exists(v1storeDir) && Files.isDirectory(v1storeDir)) { + try (Stream fileStream = Files.list(v1storeDir)) { + fileStream.filter(p -> p.toString().endsWith(XML_FILENAME_SUFFIX)).forEach(p -> { + try { + String storeName = extractStoreName(p); + storeFileConsumer.accept(p, storeName); + } catch (IOException e) { + throw new RuntimeException("could not call consumer for store file " + p + " with name " + storeName, e); + } + }); + } + } + } + + @Override + public void moveAsRepositoryStore(Path storeFile, String repositoryId) throws IOException { + Path repositoryLocation; + try { + repositoryLocation = locationResolver + .forClass(Path.class) + .getLocation(repositoryId); + } catch (IllegalStateException e) { + LOG.info("ignoring store file {} because there is no repository location for repository id {}", storeFile, repositoryId); + return; + } + Path target = repositoryLocation + .resolve(Store.DATA.getRepositoryStoreDirectory()) + .resolve(storeName); + IOUtil.mkdirs(target.toFile()); + Path resolvedSourceFile = computeV1StoreDir().resolve(storeFile); + Path resolvedTargetFile = target.resolve(storeFile.getFileName()); + LOG.trace("moving file {} to {}", resolvedSourceFile, resolvedTargetFile); + Files.move(resolvedSourceFile, resolvedTargetFile); + } + + private Path computeV1StoreDir() { + return contextProvider.getBaseDirectory().toPath().resolve("var").resolve("data").resolve(storeName); + } + + private String extractStoreName(Path p) { + String fileName = p.getFileName().toString(); + return fileName.substring(0, fileName.length() - XML_FILENAME_SUFFIX.length()); + } + }; + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/update/xml/XmlV1PropertyDAO.java b/scm-dao-xml/src/main/java/sonia/scm/update/xml/XmlV1PropertyDAO.java new file mode 100644 index 0000000000..5bdee89c68 --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/update/xml/XmlV1PropertyDAO.java @@ -0,0 +1,30 @@ +package sonia.scm.update.xml; + +import sonia.scm.store.ConfigurationEntryStore; +import sonia.scm.store.ConfigurationEntryStoreFactory; +import sonia.scm.update.V1Properties; +import sonia.scm.update.V1PropertyDAO; +import sonia.scm.update.V1PropertyReader; + +import javax.inject.Inject; +import java.util.Map; + +public class XmlV1PropertyDAO implements V1PropertyDAO { + + private final ConfigurationEntryStoreFactory configurationEntryStoreFactory; + + @Inject + public XmlV1PropertyDAO(ConfigurationEntryStoreFactory configurationEntryStoreFactory) { + this.configurationEntryStoreFactory = configurationEntryStoreFactory; + } + + @Override + public V1PropertyReader.Instance getProperties(V1PropertyReader reader) { + ConfigurationEntryStore propertyStore = configurationEntryStoreFactory + .withType(V1Properties.class) + .withName(reader.getStoreName()) + .build(); + Map all = propertyStore.getAll(); + return reader.createInstance(all); + } +} diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBPropertyFileAccessTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBPropertyFileAccessTest.java new file mode 100644 index 0000000000..629453e6c4 --- /dev/null +++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBPropertyFileAccessTest.java @@ -0,0 +1,119 @@ +package sonia.scm.store; + +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.io.DefaultFileSystem; +import sonia.scm.repository.InitialRepositoryLocationResolver; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver; +import sonia.scm.update.PropertyFileAccess; +import sonia.scm.util.IOUtil; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; + +@ExtendWith(TempDirectory.class) +@ExtendWith(MockitoExtension.class) +class JAXBPropertyFileAccessTest { + + public static final String REPOSITORY_ID = "repoId"; + public static final String STORE_NAME = "test"; + + @Mock + SCMContextProvider contextProvider; + + RepositoryLocationResolver locationResolver; + + JAXBPropertyFileAccess fileAccess; + + @BeforeEach + void initTempDir(@TempDirectory.TempDir Path tempDir) { + lenient().when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); + lenient().when(contextProvider.resolve(any())).thenAnswer(invocation -> tempDir.resolve(invocation.getArgument(0).toString())); + + locationResolver = new PathBasedRepositoryLocationResolver(contextProvider, new InitialRepositoryLocationResolver(), new DefaultFileSystem()); + + fileAccess = new JAXBPropertyFileAccess(contextProvider, locationResolver); + } + + @Test + void shouldRenameGlobalConfigFile() throws IOException { + Path baseDirectory = contextProvider.getBaseDirectory().toPath(); + Path configDirectory = baseDirectory.resolve(StoreConstants.CONFIG_DIRECTORY_NAME); + + Files.createDirectories(configDirectory); + + Path oldPath = configDirectory.resolve("old" + StoreConstants.FILE_EXTENSION); + Files.createFile(oldPath); + + fileAccess.renameGlobalConfigurationFrom("old").to("new"); + + Path newPath = configDirectory.resolve("new" + StoreConstants.FILE_EXTENSION); + assertThat(oldPath).doesNotExist(); + assertThat(newPath).exists(); + } + + @Nested + class ForExistingRepository { + + + @BeforeEach + void createRepositoryLocation() { + locationResolver.forClass(Path.class).createLocation(REPOSITORY_ID); + } + + @Test + void shouldMoveStoreFileToRepositoryBasedLocation(@TempDirectory.TempDir Path tempDir) throws IOException { + createV1StoreFile(tempDir, "myStore.xml"); + + fileAccess.forStoreName(STORE_NAME).moveAsRepositoryStore(Paths.get("myStore.xml"), REPOSITORY_ID); + + assertThat(tempDir.resolve("repositories").resolve(REPOSITORY_ID).resolve("store").resolve("data").resolve(STORE_NAME).resolve("myStore.xml")).exists(); + } + + @Test + void shouldMoveAllStoreFilesToRepositoryBasedLocations(@TempDirectory.TempDir Path tempDir) throws IOException { + locationResolver.forClass(Path.class).createLocation("repoId2"); + + createV1StoreFile(tempDir, REPOSITORY_ID + ".xml"); + createV1StoreFile(tempDir, "repoId2.xml"); + + PropertyFileAccess.StoreFileTools statisticStoreAccess = fileAccess.forStoreName(STORE_NAME); + statisticStoreAccess.forStoreFiles(statisticStoreAccess::moveAsRepositoryStore); + + assertThat(tempDir.resolve("repositories").resolve(REPOSITORY_ID).resolve("store").resolve("data").resolve(STORE_NAME).resolve("repoId.xml")).exists(); + assertThat(tempDir.resolve("repositories").resolve("repoId2").resolve("store").resolve("data").resolve(STORE_NAME).resolve("repoId2.xml")).exists(); + } + } + + private void createV1StoreFile(@TempDirectory.TempDir Path tempDir, String name) throws IOException { + Path v1Dir = tempDir.resolve("var").resolve("data").resolve(STORE_NAME); + IOUtil.mkdirs(v1Dir.toFile()); + Files.createFile(v1Dir.resolve(name)); + } + + @Nested + class ForMissingRepository { + + @Test + void shouldIgnoreStoreFile(@TempDirectory.TempDir Path tempDir) throws IOException { + createV1StoreFile(tempDir, "myStore.xml"); + + fileAccess.forStoreName(STORE_NAME).moveAsRepositoryStore(Paths.get("myStore.xml"), REPOSITORY_ID); + + assertThat(tempDir.resolve("repositories").resolve(REPOSITORY_ID).resolve("store").resolve("data").resolve(STORE_NAME).resolve("myStore.xml")).doesNotExist(); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java index 376d7cdf7a..fc5c9c9fc2 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java @@ -40,6 +40,7 @@ import org.junit.Ignore; import org.junit.Test; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.Repository; import sonia.scm.store.InMemoryConfigurationStoreFactory; import java.io.IOException; @@ -189,7 +190,7 @@ public class GitIncomingCommandTest */ private GitIncomingCommand createCommand() { - return new GitIncomingCommand(handler, new GitContext(incomingDirectory, null, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())), - incomingRepository); + return new GitIncomingCommand(handler, new GitContext(incomingDirectory, incomingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())), + this.incomingRepository); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java index 2525a6fa38..158973e710 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java @@ -40,6 +40,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Test; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.Repository; import sonia.scm.store.InMemoryConfigurationStoreFactory; import java.io.IOException; @@ -160,7 +161,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase */ private GitOutgoingCommand createCommand() { - return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory, null, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())), + return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory, outgoingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())), outgoingRepository); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java index 93eadf8935..9af1125696 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java @@ -71,7 +71,7 @@ public class LfsBlobStoreFactoryTest { // the return value (and should not be part of this test) verify(blobStoreFactory).getStore(argThat(blobStoreParameters -> { assertThat(blobStoreParameters.getName()).isEqualTo("the-id-git-lfs"); - assertThat(blobStoreParameters.getRepository()).isEqualTo(repository); + assertThat(blobStoreParameters.getRepositoryId()).isEqualTo("the-id"); return true; })); diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStoreFactory.java index 48e60684b6..1578a20751 100644 --- a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStoreFactory.java +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStoreFactory.java @@ -1,28 +1,23 @@ package sonia.scm.store; +import java.util.HashMap; +import java.util.Map; + public class InMemoryConfigurationEntryStoreFactory implements ConfigurationEntryStoreFactory { + private final Map stores = new HashMap<>(); - - - private ConfigurationEntryStore store; - - public static ConfigurationEntryStoreFactory create() { - return new InMemoryConfigurationEntryStoreFactory(new InMemoryConfigurationEntryStore()); - } - - public InMemoryConfigurationEntryStoreFactory() { - } - - public InMemoryConfigurationEntryStoreFactory(ConfigurationEntryStore store) { - this.store = store; + public static InMemoryConfigurationEntryStoreFactory create() { + return new InMemoryConfigurationEntryStoreFactory(); } @Override public ConfigurationEntryStore getStore(TypedStoreParameters storeParameters) { - if (store != null) { - return store; - } - return new InMemoryConfigurationEntryStore<>(); + String name = storeParameters.getName(); + return get(name); + } + + public InMemoryConfigurationEntryStore get(String name) { + return stores.computeIfAbsent(name, x -> new InMemoryConfigurationEntryStore()); } } diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java index ee21703c4a..aaa5b6c593 100644 --- a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java @@ -35,6 +35,9 @@ package sonia.scm.store; //~--- non-JDK imports -------------------------------------------------------- +import java.util.HashMap; +import java.util.Map; + /** * In memory configuration store factory for testing purposes. * @@ -44,24 +47,19 @@ package sonia.scm.store; */ public class InMemoryConfigurationStoreFactory implements ConfigurationStoreFactory { - private ConfigurationStore store; + private final Map stores = new HashMap<>(); - public static ConfigurationStoreFactory create() { - return new InMemoryConfigurationStoreFactory(new InMemoryConfigurationStore()); - } - - public InMemoryConfigurationStoreFactory() { - } - - public InMemoryConfigurationStoreFactory(ConfigurationStore store) { - this.store = store; + public static InMemoryConfigurationStoreFactory create() { + return new InMemoryConfigurationStoreFactory(); } @Override public ConfigurationStore getStore(TypedStoreParameters storeParameters) { - if (store != null) { - return store; - } - return new InMemoryConfigurationStore<>(); + String name = storeParameters.getName(); + return get(name); + } + + public ConfigurationStore get(String name) { + return stores.computeIfAbsent(name, x -> new InMemoryConfigurationStore()); } } diff --git a/scm-webapp/src/test/java/sonia/scm/update/UpdateStepTestUtil.java b/scm-test/src/main/java/sonia/scm/update/UpdateStepTestUtil.java similarity index 67% rename from scm-webapp/src/test/java/sonia/scm/update/UpdateStepTestUtil.java rename to scm-test/src/main/java/sonia/scm/update/UpdateStepTestUtil.java index 8326e6d255..51899cdbee 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/UpdateStepTestUtil.java +++ b/scm-test/src/main/java/sonia/scm/update/UpdateStepTestUtil.java @@ -3,11 +3,6 @@ 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; @@ -20,15 +15,13 @@ import static org.mockito.Mockito.lenient; public class UpdateStepTestUtil { -private final SCMContextProvider contextProvider; + 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())); } @@ -37,10 +30,6 @@ private final SCMContextProvider contextProvider; return contextProvider; } - public ConfigurationEntryStoreFactory getStoreFactory() { - return storeFactory; - } - public void copyConfigFile(String fileName) throws IOException { Path configDir = tempDir.resolve("config"); Files.createDirectories(configDir); @@ -53,17 +42,6 @@ private final SCMContextProvider contextProvider; copyTestDatabaseFile(configDir, fileName, targetFileName); } - public ConfigurationEntryStore 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()); diff --git a/scm-test/src/main/java/sonia/scm/update/V1PropertyDaoTestUtil.java b/scm-test/src/main/java/sonia/scm/update/V1PropertyDaoTestUtil.java new file mode 100644 index 0000000000..44020ca8a1 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/update/V1PropertyDaoTestUtil.java @@ -0,0 +1,40 @@ +package sonia.scm.update; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.Arrays.stream; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class V1PropertyDaoTestUtil { + + private final V1PropertyDAO propertyDAO = mock(V1PropertyDAO.class); + + public V1PropertyDAO getPropertyDAO() { + return propertyDAO; + } + + public void mockRepositoryProperties(PropertiesForRepository... mockedPropertiesForRepositories) { + Map map = new HashMap<>(); + stream(mockedPropertiesForRepositories).forEach(p -> map.put(p.repositoryId, p.asProperties())); + V1PropertyReader.Instance v1PropertyReader = new MapBasedPropertyReaderInstance(map); + when(propertyDAO.getProperties(argThat(argument -> argument instanceof RepositoryV1PropertyReader))).thenReturn(v1PropertyReader); + } + + public static class PropertiesForRepository { + private final String repositoryId; + private final Map properties; + + public PropertiesForRepository(String repositoryId, Map properties) { + this.repositoryId = repositoryId; + this.properties = properties; + } + + V1Properties asProperties() { + return new V1Properties(properties.entrySet().stream().map(e -> new V1Property(e.getKey(), e.getValue())).collect(Collectors.toList())); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java index 57c05b9d21..4765d9b96e 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java @@ -24,6 +24,10 @@ import sonia.scm.store.FileBlobStoreFactory; import sonia.scm.store.JAXBConfigurationEntryStoreFactory; import sonia.scm.store.JAXBConfigurationStoreFactory; import sonia.scm.store.JAXBDataStoreFactory; +import sonia.scm.store.JAXBPropertyFileAccess; +import sonia.scm.update.PropertyFileAccess; +import sonia.scm.update.V1PropertyDAO; +import sonia.scm.update.xml.XmlV1PropertyDAO; public class BootstrapModule extends AbstractModule { @@ -60,6 +64,8 @@ public class BootstrapModule extends AbstractModule { bind(DataStoreFactory.class, JAXBDataStoreFactory.class); bind(BlobStoreFactory.class, FileBlobStoreFactory.class); bind(PluginLoader.class).toInstance(pluginLoader); + bind(V1PropertyDAO.class, XmlV1PropertyDAO.class); + bind(PropertyFileAccess.class, JAXBPropertyFileAccess.class); } private void bind(Class clazz, Class defaultImplementation) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/CoreUpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/CoreUpdateStep.java new file mode 100644 index 0000000000..c0468eef39 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/CoreUpdateStep.java @@ -0,0 +1,6 @@ +package sonia.scm.update; + +import sonia.scm.migration.UpdateStep; + +public interface CoreUpdateStep extends UpdateStep { +} 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 910d9ee054..a3c0ffd875 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java +++ b/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java @@ -32,12 +32,20 @@ public class UpdateEngine { private List sortSteps(Set steps) { LOG.trace("sorting available update steps:"); List sortedSteps = steps.stream() - .sorted(Comparator.comparing(UpdateStep::getTargetVersion).reversed()) + .sorted( + Comparator + .comparing(UpdateStep::getTargetVersion) + .thenComparing(this::isCoreUpdateStep) + .reversed()) .collect(toList()); sortedSteps.forEach(step -> LOG.trace("{} for version {}", step.getAffectedDataType(), step.getTargetVersion())); return sortedSteps; } + private boolean isCoreUpdateStep(UpdateStep updateStep) { + return updateStep instanceof CoreUpdateStep; + } + public void update() { steps .stream() diff --git a/scm-webapp/src/main/java/sonia/scm/update/group/XmlGroupV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/group/XmlGroupV1UpdateStep.java index 7130ead4a9..23eb477e09 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/group/XmlGroupV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/group/XmlGroupV1UpdateStep.java @@ -11,7 +11,7 @@ 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.update.V1Properties; import sonia.scm.version.Version; import javax.inject.Inject; @@ -30,6 +30,7 @@ import java.util.Optional; import static java.util.Optional.empty; import static java.util.Optional.of; +import static sonia.scm.update.V1PropertyReader.GROUP_PROPERTY_READER; import static sonia.scm.version.Version.parse; @Extension @@ -51,7 +52,7 @@ public class XmlGroupV1UpdateStep implements UpdateStep { this.groupDAO = groupDAO; this.propertyStore = configurationEntryStoreFactory .withType(V1Properties.class) - .withName("group-properties-v1") + .withName(GROUP_PROPERTY_READER.getStoreName()) .build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/update/properties/V1Properties.java b/scm-webapp/src/main/java/sonia/scm/update/properties/V1Properties.java deleted file mode 100644 index 3c8555a76e..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/update/properties/V1Properties.java +++ /dev/null @@ -1,14 +0,0 @@ -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 properties; -} diff --git a/scm-webapp/src/main/java/sonia/scm/update/properties/V1Property.java b/scm-webapp/src/main/java/sonia/scm/update/properties/V1Property.java deleted file mode 100644 index c92c15188d..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/update/properties/V1Property.java +++ /dev/null @@ -1,10 +0,0 @@ -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; -} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java b/scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java index 256e2ad397..4ce823bd33 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java @@ -1,6 +1,6 @@ package sonia.scm.update.repository; -import sonia.scm.update.properties.V1Properties; +import sonia.scm.update.V1Properties; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index ad20b59529..f7a4e1ed37 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -5,7 +5,6 @@ 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; @@ -13,7 +12,8 @@ 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.update.CoreUpdateStep; +import sonia.scm.update.V1Properties; import sonia.scm.version.Version; import javax.inject.Inject; @@ -36,6 +36,7 @@ import java.util.stream.Stream; import static java.util.Collections.emptyList; import static java.util.Optional.empty; import static java.util.Optional.of; +import static sonia.scm.update.V1PropertyReader.REPOSITORY_PROPERTY_READER; import static sonia.scm.version.Version.parse; /** @@ -56,7 +57,7 @@ import static sonia.scm.version.Version.parse; * */ @Extension -public class XmlRepositoryV1UpdateStep implements UpdateStep { +public class XmlRepositoryV1UpdateStep implements CoreUpdateStep { private static Logger LOG = LoggerFactory.getLogger(XmlRepositoryV1UpdateStep.class); @@ -80,7 +81,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { this.injector = injector; this.propertyStore = configurationEntryStoreFactory .withType(V1Properties.class) - .withName("repository-properties-v1") + .withName(REPOSITORY_PROPERTY_READER.getStoreName()) .build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/update/user/XmlUserV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/user/XmlUserV1UpdateStep.java index 24d96d3654..b2da69fd9b 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/user/XmlUserV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/user/XmlUserV1UpdateStep.java @@ -10,7 +10,7 @@ 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.update.V1Properties; import sonia.scm.user.User; import sonia.scm.user.xml.XmlUserDAO; import sonia.scm.version.Version; @@ -31,6 +31,7 @@ import java.util.Optional; import static java.util.Optional.empty; import static java.util.Optional.of; +import static sonia.scm.update.V1PropertyReader.USER_PROPERTY_READER; import static sonia.scm.version.Version.parse; @Extension @@ -50,7 +51,7 @@ public class XmlUserV1UpdateStep implements UpdateStep { this.configurationEntryStoreFactory = configurationEntryStoreFactory; this.propertyStore = configurationEntryStoreFactory .withType(V1Properties.class) - .withName("user-properties-v1") + .withName(USER_PROPERTY_READER.getStoreName()) .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 02ff0967bb..f94d7cd27a 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java @@ -16,7 +16,7 @@ import static sonia.scm.version.Version.parse; class UpdateEngineTest { - ConfigurationEntryStoreFactory storeFactory = new InMemoryConfigurationEntryStoreFactory(new InMemoryConfigurationEntryStore()); + ConfigurationEntryStoreFactory storeFactory = new InMemoryConfigurationEntryStoreFactory(); List processedUpdates = new ArrayList<>(); @@ -32,7 +32,21 @@ class UpdateEngineTest { updateEngine.update(); assertThat(processedUpdates) - .containsExactly("1.1.0", "1.1.1", "1.2.0"); + .containsExactly("test:1.1.0", "test:1.1.1", "test:1.2.0"); + } + + @Test + void shouldProcessCoreStepsBeforeOther() { + LinkedHashSet updateSteps = new LinkedHashSet<>(); + + updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0")); + updateSteps.add(new CoreFixedVersionUpdateStep("core", "1.2.0")); + + UpdateEngine updateEngine = new UpdateEngine(updateSteps, storeFactory); + updateEngine.update(); + + assertThat(processedUpdates) + .containsExactly("core:1.2.0", "test:1.2.0"); } @Test @@ -67,7 +81,7 @@ class UpdateEngineTest { updateEngine = new UpdateEngine(updateSteps, storeFactory); updateEngine.update(); - assertThat(processedUpdates).containsExactly("1.1.1"); + assertThat(processedUpdates).containsExactly("other:1.1.1"); } class FixedVersionUpdateStep implements UpdateStep { @@ -91,7 +105,13 @@ class UpdateEngineTest { @Override public void doUpdate() { - processedUpdates.add(version); + processedUpdates.add(type + ":" + version); + } + } + + class CoreFixedVersionUpdateStep extends FixedVersionUpdateStep implements CoreUpdateStep { + CoreFixedVersionUpdateStep(String type, String version) { + super(type, version); } } } diff --git a/scm-webapp/src/test/java/sonia/scm/update/group/XmlGroupV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/group/XmlGroupV1UpdateStepTest.java index 6dee5fd5c1..9d550e20b1 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/group/XmlGroupV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/group/XmlGroupV1UpdateStepTest.java @@ -11,7 +11,11 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.group.Group; import sonia.scm.group.xml.XmlGroupDAO; +import sonia.scm.store.ConfigurationEntryStore; +import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; import sonia.scm.update.UpdateStepTestUtil; +import sonia.scm.update.V1Properties; +import sonia.scm.update.V1Property; import javax.xml.bind.JAXBException; import java.io.IOException; @@ -20,11 +24,11 @@ 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; +import static sonia.scm.store.InMemoryConfigurationEntryStoreFactory.create; @ExtendWith(MockitoExtension.class) @ExtendWith(TempDirectory.class) @@ -36,6 +40,8 @@ class XmlGroupV1UpdateStepTest { @Captor ArgumentCaptor groupCaptor; + InMemoryConfigurationEntryStoreFactory storeFactory = create(); + XmlGroupV1UpdateStep updateStep; private UpdateStepTestUtil testUtil; @@ -44,7 +50,7 @@ class XmlGroupV1UpdateStepTest { @BeforeEach void mockScmHome(@TempDirectory.TempDir Path tempDir) { testUtil = new UpdateStepTestUtil(tempDir); - updateStep = new XmlGroupV1UpdateStep(testUtil.getContextProvider(), groupDAO, testUtil.getStoreFactory()); + updateStep = new XmlGroupV1UpdateStep(testUtil.getContextProvider(), groupDAO, storeFactory); } @Nested @@ -83,19 +89,10 @@ class XmlGroupV1UpdateStepTest { @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( - "normals", - "", - "", - "mostly", - "humans", - "", - ""); + ConfigurationEntryStore propertiesStore = storeFactory.get("group-properties-v1"); + V1Properties properties = propertiesStore.get("normals"); + assertThat(properties).isNotNull(); + assertThat(properties.get("mostly")).isEqualTo("humans"); } } diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java index 6c977a3cb6..2145164062 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java @@ -51,7 +51,7 @@ class XmlRepositoryV1UpdateStepTest { @Mock MigrationStrategyDao migrationStrategyDao; - ConfigurationEntryStoreFactory configurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(new InMemoryConfigurationEntryStore()); + InMemoryConfigurationEntryStoreFactory configurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(); @Captor ArgumentCaptor storeCaptor; @@ -137,7 +137,7 @@ class XmlRepositoryV1UpdateStepTest { void shouldExtractPropertiesFromRepositories() throws JAXBException { updateStep.doUpdate(); - ConfigurationEntryStore store = configurationEntryStoreFactory.withType(null).withName("").build(); + ConfigurationEntryStore store = configurationEntryStoreFactory.get("repository-properties-v1"); assertThat(store.getAll()) .hasSize(3); } diff --git a/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java index f8949d73a5..d0115e3426 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java @@ -26,6 +26,7 @@ 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; +import static sonia.scm.store.InMemoryConfigurationEntryStoreFactory.create; @ExtendWith(MockitoExtension.class) @ExtendWith(TempDirectory.class) @@ -40,8 +41,8 @@ class XmlSecurityV1UpdateStepTest { @BeforeEach void mockScmHome(@TempDirectory.TempDir Path tempDir) { when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); - assignedPermissionStore = new InMemoryConfigurationEntryStore<>(); - ConfigurationEntryStoreFactory inMemoryConfigurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(assignedPermissionStore); + InMemoryConfigurationEntryStoreFactory inMemoryConfigurationEntryStoreFactory = create(); + assignedPermissionStore = inMemoryConfigurationEntryStoreFactory.get("security"); updateStep = new XmlSecurityV1UpdateStep(contextProvider, inMemoryConfigurationEntryStoreFactory); } diff --git a/scm-webapp/src/test/java/sonia/scm/update/user/XmlUserV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/user/XmlUserV1UpdateStepTest.java index 19d37d542d..ce3a4a391e 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/user/XmlUserV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/user/XmlUserV1UpdateStepTest.java @@ -10,7 +10,11 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.security.AssignedPermission; +import sonia.scm.store.ConfigurationEntryStore; +import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; import sonia.scm.update.UpdateStepTestUtil; +import sonia.scm.update.V1Properties; +import sonia.scm.update.V1Property; import sonia.scm.user.User; import sonia.scm.user.xml.XmlUserDAO; @@ -20,11 +24,11 @@ import java.nio.file.Path; import java.util.Optional; 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; +import static sonia.scm.store.InMemoryConfigurationEntryStoreFactory.create; @ExtendWith(MockitoExtension.class) @ExtendWith(TempDirectory.class) @@ -36,6 +40,8 @@ class XmlUserV1UpdateStepTest { @Captor ArgumentCaptor userCaptor; + InMemoryConfigurationEntryStoreFactory storeFactory = create(); + XmlUserV1UpdateStep updateStep; private UpdateStepTestUtil testUtil; @@ -43,7 +49,7 @@ class XmlUserV1UpdateStepTest { @BeforeEach void mockScmHome(@TempDirectory.TempDir Path tempDir) { testUtil = new UpdateStepTestUtil(tempDir); - updateStep = new XmlUserV1UpdateStep(testUtil.getContextProvider(), userDAO, testUtil.getStoreFactory()); + updateStep = new XmlUserV1UpdateStep(testUtil.getContextProvider(), userDAO, storeFactory); } @Nested @@ -63,7 +69,7 @@ class XmlUserV1UpdateStepTest { void shouldCreateNewPermissionsForV1AdminUser() throws JAXBException { updateStep.doUpdate(); Optional assignedPermission = - testUtil.getStoreForConfigFile("security") + storeFactory.get("security") .getAll() .values() .stream() @@ -98,23 +104,11 @@ class XmlUserV1UpdateStepTest { @Test void shouldExtractProperties() throws JAXBException { updateStep.doUpdate(); - Path propertiesFile = testUtil.getFile("user-properties-v1.xml"); - assertThat(propertiesFile) - .exists(); - assertThat(linesOf(propertiesFile.toFile())) - .extracting(String::trim) - .containsSequence( - "dent", - "", - "", - "born.on", - "earth", - "", - "", - "last.seen", - "end of the universe", - "", - ""); + ConfigurationEntryStore propertiesStore = storeFactory.get("user-properties-v1"); + V1Properties properties = propertiesStore.get("dent"); + assertThat(properties).isNotNull(); + assertThat(properties.get("born.on")).isEqualTo("earth"); + assertThat(properties.get("last.seen")).isEqualTo("end of the universe"); } }