diff --git a/gradle/changelog/mark_config_entry_stores.yaml b/gradle/changelog/mark_config_entry_stores.yaml new file mode 100644 index 0000000000..63007ad8d2 --- /dev/null +++ b/gradle/changelog/mark_config_entry_stores.yaml @@ -0,0 +1,4 @@ +- type: added + description: XML attribute in root element of config entry stores ([#1545](https://github.com/scm-manager/scm-manager/pull/1545)) +- type: changed + description: Config entry stores are handled explicitly in exports ([#1545](https://github.com/scm-manager/scm-manager/pull/1545)) diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/ExportableBlobFileStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableBlobFileStore.java index d2b45837f3..c76515fcfe 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/ExportableBlobFileStore.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableBlobFileStore.java @@ -33,7 +33,7 @@ import static java.util.Optional.of; class ExportableBlobFileStore extends ExportableDirectoryBasedFileStore { - static Function>> BLOB_FACTORY = + static final Function>> BLOB_FACTORY = storeType -> storeType == StoreType.BLOB ? of(ExportableBlobFileStore::new) : empty(); ExportableBlobFileStore(Path directory) { diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/ExportableConfigEntryFileStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableConfigEntryFileStore.java new file mode 100644 index 0000000000..b5489f534e --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableConfigEntryFileStore.java @@ -0,0 +1,56 @@ +/* + * 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 java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.Function; + +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static sonia.scm.store.ExportCopier.putFileContentIntoStream; + +class ExportableConfigEntryFileStore implements ExportableStore { + + private final Path file; + + static final Function>> CONFIG_ENTRY_FACTORY = + storeType -> storeType == StoreType.CONFIG_ENTRY ? of(ExportableConfigEntryFileStore::new) : empty(); + + ExportableConfigEntryFileStore(Path file) { + this.file = file; + } + + @Override + public StoreEntryMetaData getMetaData() { + return new StoreEntryMetaData(StoreType.CONFIG_ENTRY, file.getFileName().toString()); + } + + @Override + public void export(Exporter exporter) throws IOException { + putFileContentIntoStream(exporter, file); + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/ExportableConfigFileStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableConfigFileStore.java index b2decc4cf0..3ae547affb 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/ExportableConfigFileStore.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableConfigFileStore.java @@ -37,7 +37,7 @@ class ExportableConfigFileStore implements ExportableStore { private final Path file; - static Function>> CONFIG_FACTORY = + static final Function>> CONFIG_FACTORY = storeType -> storeType == StoreType.CONFIG ? of(ExportableConfigFileStore::new) : empty(); ExportableConfigFileStore(Path file) { @@ -51,8 +51,6 @@ class ExportableConfigFileStore implements ExportableStore { @Override public void export(Exporter exporter) throws IOException { - if (file.getFileName().toString().endsWith(".xml")) { - putFileContentIntoStream(exporter, file); - } + putFileContentIntoStream(exporter, file); } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/ExportableDataFileStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableDataFileStore.java index 4841775e49..97b90d7702 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/ExportableDataFileStore.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/ExportableDataFileStore.java @@ -33,7 +33,7 @@ import static java.util.Optional.of; class ExportableDataFileStore extends ExportableDirectoryBasedFileStore { - static Function>> DATA_FACTORY = + static final Function>> DATA_FACTORY = storeType -> storeType == StoreType.DATA ? of(ExportableDataFileStore::new) : empty(); ExportableDataFileStore(Path directory) { diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileStoreExporter.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileStoreExporter.java index c81e76c46d..437dbc8336 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/FileStoreExporter.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileStoreExporter.java @@ -24,11 +24,16 @@ package sonia.scm.store; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.repository.api.ExportFailedException; +import sonia.scm.xml.XmlStreams; import javax.inject.Inject; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -43,15 +48,18 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static sonia.scm.ContextEntry.ContextBuilder.noContext; import static sonia.scm.store.ExportableBlobFileStore.BLOB_FACTORY; +import static sonia.scm.store.ExportableConfigEntryFileStore.CONFIG_ENTRY_FACTORY; import static sonia.scm.store.ExportableConfigFileStore.CONFIG_FACTORY; import static sonia.scm.store.ExportableDataFileStore.DATA_FACTORY; public class FileStoreExporter implements StoreExporter { - private final RepositoryLocationResolver locationResolver; - private static final Collection>>> STORE_FACTORIES = - asList(DATA_FACTORY, BLOB_FACTORY, CONFIG_FACTORY); + asList(DATA_FACTORY, BLOB_FACTORY, CONFIG_FACTORY, CONFIG_ENTRY_FACTORY); + + private static final Logger LOG = LoggerFactory.getLogger(FileStoreExporter.class); + + private final RepositoryLocationResolver locationResolver; @Inject public FileStoreExporter(RepositoryLocationResolver locationResolver) { @@ -103,19 +111,45 @@ public class FileStoreExporter implements StoreExporter { private Optional getStoreFor(Path storePath) { return STORE_FACTORIES .stream() - .map(factory -> factory.apply(getEnumForValue(storePath.getParent()))) + .map(factory -> factory.apply(getEnumForValue(storePath))) .filter(Optional::isPresent) .map(Optional::get) .findFirst() .map(f -> f.apply(storePath)); } - private StoreType getEnumForValue(Path storeTypeDirectory) { - for (StoreType type : StoreType.values()) { - if (type.getValue().equals(storeTypeDirectory.getFileName().toString())) { - return type; + private StoreType getEnumForValue(Path storePath) { + if (Files.isDirectory(storePath)) { + for (StoreType type : StoreType.values()) { + if (type.getValue().equals(storePath.getParent().getFileName().toString())) { + return type; + } } + } else if (storePath.toString().endsWith(".xml")) { + return determineConfigType(storePath); + } else { + LOG.info("ignoring unknown file '{}' in export", storePath); } return null; } + + private StoreType determineConfigType(Path storePath) { + XMLStreamReader reader = null; + try { + reader = XmlStreams.createReader(storePath); + reader.nextTag(); + if ( + "configuration".equals(reader.getLocalName()) + && "config-entry".equals(reader.getAttributeValue(0)) + && "type".equals(reader.getAttributeName(0).getLocalPart())) { + return StoreType.CONFIG_ENTRY; + } else { + return StoreType.CONFIG; + } + } catch (XMLStreamException | IOException e) { + throw new ExportFailedException(noContext(), "Failed to read store file " + storePath, e); + } finally { + XmlStreams.close(reader); + } + } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java index b5d20aad53..714067036d 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java @@ -195,7 +195,7 @@ public class JAXBConfigurationEntryStore implements ConfigurationEntryStore e : entries.entrySet()) { diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/ExportableFileStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/ExportableFileStoreTest.java index 25437031b8..29a46a24be 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/ExportableFileStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/ExportableFileStoreTest.java @@ -91,16 +91,6 @@ class ExportableFileStoreTest { assertThat(os.toString()).isNotBlank(); } - @Test - void shouldFilterNoneConfigFiles(@TempDir Path temp) throws IOException { - createFile(temp, "config", "", "first.bck"); - ExportableStore exportableConfigFileStore = new ExportableConfigFileStore(temp.resolve("config").resolve("first.bck")); - - exportableConfigFileStore.export(exporter); - - verify(exporter, never()).put(anyString(), anyLong()); - } - @Test void shouldPutContentIntoExporterForBlobStore(@TempDir Path temp) throws IOException { createFile(temp, "blob", "assets", "first.blob"); diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/FileStoreExporterTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/FileStoreExporterTest.java index d7677e94a4..4e4c9d2173 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/FileStoreExporterTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/FileStoreExporterTest.java @@ -24,6 +24,7 @@ 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; @@ -38,7 +39,8 @@ import sonia.scm.repository.RepositoryTestData; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; +import java.nio.file.Paths; +import java.util.Arrays; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -55,6 +57,14 @@ class FileStoreExporterTest { @InjectMocks private FileStoreExporter fileStoreExporter; + private Path storePath; + + @BeforeEach + void setUpStorePath(@TempDir Path temp) { + storePath = temp.resolve("store"); + when(resolver.forClass(Path.class).getLocation(REPOSITORY.getId())).thenReturn(temp); + } + @Test void shouldReturnEmptyList(@TempDir Path temp) { when(resolver.forClass(Path.class).getLocation(REPOSITORY.getId())).thenReturn(temp); @@ -64,27 +74,57 @@ class FileStoreExporterTest { } @Test - void shouldReturnListOfExportableStores(@TempDir Path temp) throws IOException { - Path storePath = temp.resolve("store"); - createFile(storePath, StoreType.CONFIG.getValue(), null, "first.xml"); - createFile(storePath, StoreType.DATA.getValue(), "ci", "second.xml"); - createFile(storePath, StoreType.DATA.getValue(), "jenkins", "third.xml"); - when(resolver.forClass(Path.class).getLocation(REPOSITORY.getId())).thenReturn(temp); + void shouldReturnConfigStores() throws IOException { + createFile(StoreType.CONFIG.getValue(), "config.xml") + .withContent("", "", "some arbitrary content", ""); List exportableStores = fileStoreExporter.listExportableStores(REPOSITORY); - assertThat(exportableStores).hasSize(3); + assertThat(exportableStores).hasSize(1); assertThat(exportableStores.stream().filter(e -> e.getMetaData().getType().equals(StoreType.CONFIG))).hasSize(1); + } + + @Test + void shouldReturnConfigEntryStores() throws IOException { + createFile(StoreType.CONFIG.getValue(), "config-entry.xml") + .withContent("", "", ""); + + List exportableStores = fileStoreExporter.listExportableStores(REPOSITORY); + + assertThat(exportableStores).hasSize(1); + assertThat(exportableStores.stream().filter(e -> e.getMetaData().getType().equals(StoreType.CONFIG_ENTRY))).hasSize(1); + } + + @Test + void shouldReturnDataStores() throws IOException { + createFile(StoreType.DATA.getValue(), "ci", "data.xml"); + createFile(StoreType.DATA.getValue(), "jenkins", "data.xml"); + + List exportableStores = fileStoreExporter.listExportableStores(REPOSITORY); + + assertThat(exportableStores).hasSize(2); assertThat(exportableStores.stream().filter(e -> e.getMetaData().getType().equals(StoreType.DATA))).hasSize(2); } - private void createFile(Path storePath, String type, String name, String fileName) throws IOException { - Path path = name != null ? storePath.resolve(type).resolve(name) : storePath.resolve(type); - Files.createDirectories(path); - Path file = path.resolve(fileName); + private FileWriter createFile(String... names) throws IOException { + Path file = Arrays.stream(names).map(Paths::get).reduce(Path::resolve).map(storePath::resolve).orElse(storePath); + Files.createDirectories(file.getParent()); if (!Files.exists(file)) { Files.createFile(file); } - Files.write(file, Collections.singletonList("something")); + return new FileWriter(file); + } + + private static class FileWriter { + + private final Path file; + + private FileWriter(Path file) { + this.file = file; + } + + void withContent(String... content) throws IOException { + Files.write(file, Arrays.asList(content)); + } } } diff --git a/scm-test/src/main/java/sonia/scm/AbstractTestBase.java b/scm-test/src/main/java/sonia/scm/AbstractTestBase.java index 535b84bfc2..e6b5e1a305 100644 --- a/scm-test/src/main/java/sonia/scm/AbstractTestBase.java +++ b/scm-test/src/main/java/sonia/scm/AbstractTestBase.java @@ -66,9 +66,6 @@ public class AbstractTestBase private File tempDirectory; - protected DefaultFileSystem fileSystem; - - protected RepositoryDAO repositoryDAO = mock(RepositoryDAO.class); protected RepositoryLocationResolver repositoryLocationResolver; @BeforeEach @@ -79,7 +76,6 @@ public class AbstractTestBase UUID.randomUUID().toString()); assertTrue(tempDirectory.mkdirs()); contextProvider = MockUtil.getSCMContextProvider(tempDirectory); - fileSystem = new DefaultFileSystem(); InitialRepositoryLocationResolver initialRepoLocationResolver = new InitialRepositoryLocationResolver(); repositoryLocationResolver = new TempDirRepositoryLocationResolver(tempDirectory); postSetUp(); diff --git a/scm-webapp/src/main/java/sonia/scm/importexport/TarArchiveRepositoryStoreImporter.java b/scm-webapp/src/main/java/sonia/scm/importexport/TarArchiveRepositoryStoreImporter.java index 0f3b586a43..6a6bd702c4 100644 --- a/scm-webapp/src/main/java/sonia/scm/importexport/TarArchiveRepositoryStoreImporter.java +++ b/scm-webapp/src/main/java/sonia/scm/importexport/TarArchiveRepositoryStoreImporter.java @@ -63,17 +63,17 @@ public class TarArchiveRepositoryStoreImporter { private void importStoreByType(Repository repository, TarArchiveInputStream tais, String[] entryPathParts) { String storeType = entryPathParts[1]; - if (storeType.equals(StoreType.DATA.getValue())) { + if (isDataStore(storeType)) { repositoryStoreImporter .doImport(repository) .importStore(new StoreEntryMetaData(StoreType.DATA, entryPathParts[2])) .importEntry(entryPathParts[3], tais); - } else if (storeType.equals(StoreType.CONFIG.getValue())){ + } else if (isConfigStore(storeType)){ repositoryStoreImporter .doImport(repository) .importStore(new StoreEntryMetaData(StoreType.CONFIG, "")) .importEntry(entryPathParts[2], tais); - } else if(storeType.equals(StoreType.BLOB.getValue())) { + } else if(isBlobStore(storeType)) { repositoryStoreImporter .doImport(repository) .importStore(new StoreEntryMetaData(StoreType.BLOB, entryPathParts[2])) @@ -94,16 +94,28 @@ public class TarArchiveRepositoryStoreImporter { //This prevents array out of bound exceptions if (entryPathParts.length > 1) { String storeType = entryPathParts[1]; - if (storeType.equals(StoreType.DATA.getValue()) || storeType.equals(StoreType.BLOB.getValue())) { + if (isDataStore(storeType) || isBlobStore(storeType)) { return entryPathParts.length == 4; } - if (storeType.equals(StoreType.CONFIG.getValue())) { + if (isConfigStore(storeType)) { return entryPathParts.length == 3; } } return false; } + private boolean isBlobStore(String storeType) { + return storeType.equals(StoreType.BLOB.getValue()); + } + + private boolean isDataStore(String storeType) { + return storeType.equals(StoreType.DATA.getValue()); + } + + private boolean isConfigStore(String storeType) { + return storeType.equals(StoreType.CONFIG.getValue()) || storeType.equals(StoreType.CONFIG_ENTRY.getValue()); + } + static class NoneClosingTarArchiveInputStream extends TarArchiveInputStream { public NoneClosingTarArchiveInputStream(InputStream is) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/store/DifferentiateBetweenConfigAndConfigEntryForGlobalStoreUpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/store/DifferentiateBetweenConfigAndConfigEntryForGlobalStoreUpdateStep.java new file mode 100644 index 0000000000..88a8024c6f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/store/DifferentiateBetweenConfigAndConfigEntryForGlobalStoreUpdateStep.java @@ -0,0 +1,50 @@ +/* + * 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.store; + +import sonia.scm.SCMContextProvider; +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; + +import javax.inject.Inject; +import java.io.File; +import java.nio.file.Path; + +@Extension +public class DifferentiateBetweenConfigAndConfigEntryForGlobalStoreUpdateStep extends DifferentiateBetweenConfigAndConfigEntryUpdateStep implements UpdateStep { + + private final SCMContextProvider contextProvider; + + @Inject + public DifferentiateBetweenConfigAndConfigEntryForGlobalStoreUpdateStep(SCMContextProvider contextProvider) { + this.contextProvider = contextProvider; + } + + @Override + public void doUpdate() throws Exception { + Path configPath = new File(contextProvider.getBaseDirectory(), "config").toPath(); + updateAllInDirectory(configPath); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/store/DifferentiateBetweenConfigAndConfigEntryForRepositoryStoresUpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/store/DifferentiateBetweenConfigAndConfigEntryForRepositoryStoresUpdateStep.java new file mode 100644 index 0000000000..585bc50035 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/store/DifferentiateBetweenConfigAndConfigEntryForRepositoryStoresUpdateStep.java @@ -0,0 +1,49 @@ +/* + * 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.store; + +import sonia.scm.migration.RepositoryUpdateContext; +import sonia.scm.migration.RepositoryUpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.RepositoryLocationResolver; + +import javax.inject.Inject; +import java.nio.file.Path; + +@Extension +public class DifferentiateBetweenConfigAndConfigEntryForRepositoryStoresUpdateStep extends DifferentiateBetweenConfigAndConfigEntryUpdateStep implements RepositoryUpdateStep { + + private final RepositoryLocationResolver locationResolver; + + @Inject + public DifferentiateBetweenConfigAndConfigEntryForRepositoryStoresUpdateStep(RepositoryLocationResolver locationResolver) { + this.locationResolver = locationResolver; + } + + @Override + public void doUpdate(RepositoryUpdateContext repositoryUpdateContext) throws Exception { + updateAllInDirectory(locationResolver.forClass(Path.class).getLocation(repositoryUpdateContext.getRepositoryId())); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/store/DifferentiateBetweenConfigAndConfigEntryUpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/store/DifferentiateBetweenConfigAndConfigEntryUpdateStep.java new file mode 100644 index 0000000000..2c2c457bbe --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/store/DifferentiateBetweenConfigAndConfigEntryUpdateStep.java @@ -0,0 +1,131 @@ +/* + * 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.store; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; +import sonia.scm.migration.UpdateException; +import sonia.scm.store.CopyOnWrite; +import sonia.scm.version.Version; +import sonia.scm.xml.XmlStreams; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +abstract class DifferentiateBetweenConfigAndConfigEntryUpdateStep { + + private static final Logger LOG = LoggerFactory.getLogger(DifferentiateBetweenConfigAndConfigEntryUpdateStep.class); + + public Version getTargetVersion() { + return Version.parse("2.0.0"); + } + + public String getAffectedDataType() { + return "sonia.scm.dao.xml"; + } + + void updateAllInDirectory(Path configDirectory) throws IOException { + try (Stream list = Files.list(configDirectory)) { + list + .filter(Files::isRegularFile) + .filter(file -> file.toString().endsWith(".xml")) + .filter(this::isConfigEntryFile) + .forEach(this::updateSingleFile); + } + } + + private boolean isConfigEntryFile(Path potentialFile) { + LOG.trace("Testing whether file is config entry file without mark: {}", potentialFile); + XMLStreamReader reader = null; + try { + reader = XmlStreams.createReader(potentialFile); + reader.nextTag(); + if ("configuration".equals(reader.getLocalName())) { + reader.nextTag(); + if ("entry".equals(reader.getLocalName())) { + return true; + } + } + } catch (XMLStreamException | IOException e) { + throw new UpdateException("Error reading file " + potentialFile, e); + } finally { + XmlStreams.close(reader); + } + return false; + } + + private void updateSingleFile(Path configFile) { + LOG.info("Updating config entry file: {}", configFile); + + Document configEntryDocument = readAsXmlDocument(configFile); + + configEntryDocument.getDocumentElement().setAttribute("type", "config-entry"); + + CopyOnWrite.withTemporaryFile( + temporaryFile -> writeXmlDocument(configEntryDocument, temporaryFile), configFile + ); + } + + private void writeXmlDocument(Document configEntryDocument, Path temporaryFile) throws TransformerException { + try { + TransformerFactory factory = TransformerFactory.newInstance(); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + Transformer transformer = factory.newTransformer(); + DOMSource domSource = new DOMSource(configEntryDocument); + StreamResult streamResult = new StreamResult(Files.newOutputStream(temporaryFile)); + transformer.transform(domSource, streamResult); + } catch (IOException e) { + throw new UpdateException("Could not write modified config entry file", e); + } + } + + private Document readAsXmlDocument(Path configFile) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(Files.newInputStream(configFile)); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw new UpdateException("Could not read config entry file", e); + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/update/store/DifferentiateBetweenConfigAndConfigEntryUpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/store/DifferentiateBetweenConfigAndConfigEntryUpdateStepTest.java new file mode 100644 index 0000000000..0eea1f959e --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/update/store/DifferentiateBetweenConfigAndConfigEntryUpdateStepTest.java @@ -0,0 +1,109 @@ +/* + * 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.store; + +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; + +import static com.google.common.io.Resources.copy; +import static com.google.common.io.Resources.getResource; +import static java.nio.file.Files.newOutputStream; +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("UnstableApiUsage") +class DifferentiateBetweenConfigAndConfigEntryUpdateStepTest { + + @Test + void shouldNotModifyConfigFile(@TempDir Path temp) throws IOException { + Path configFile = temp.resolve("some.store.xml"); + copy( + getResource("sonia/scm/update/store/config_file.xml.content"), + newOutputStream(configFile)); + + new DifferentiateBetweenConfigAndConfigEntryUpdateStep() {}.updateAllInDirectory(temp); + + assertContent(configFile, "sonia/scm/update/store/config_file.xml.content"); + } + + @Test + void shouldNotModifySingleLineFile(@TempDir Path temp) throws IOException { + String singleLineContent = ""; + Path configFile = temp.resolve("some.store.xml"); + Files.write(configFile, Collections.singletonList(singleLineContent)); + + new DifferentiateBetweenConfigAndConfigEntryUpdateStep() {}.updateAllInDirectory(temp); + + assertThat(configFile).hasContent(singleLineContent); + } + + @Test + @SuppressWarnings("java:S2699") // we just want to make sure no exception it thrown + void shouldNotConsiderDirectories(@TempDir Path temp) throws IOException { + Path configFile = temp.resolve("some.store.xml"); + Files.createDirectories(configFile); + + new DifferentiateBetweenConfigAndConfigEntryUpdateStep() {}.updateAllInDirectory(temp); + + // no exception expected + } + + @Test + void shouldIgnoreFilesWithoutXmlSuffix(@TempDir Path temp) throws IOException { + Path otherFile = temp.resolve("some.other.file"); + copy( + getResource("sonia/scm/update/store/config_entry_file_without_mark.xml.content"), + newOutputStream(otherFile)); + + new DifferentiateBetweenConfigAndConfigEntryUpdateStep() {}.updateAllInDirectory(temp); + + assertContent(otherFile, "sonia/scm/update/store/config_entry_file_without_mark.xml.content"); + } + + @Test + void shouldHandleConfigEntryFile(@TempDir Path temp) throws IOException { + Path configFile = temp.resolve("some.store.xml"); + copy( + getResource("sonia/scm/update/store/config_entry_file_without_mark.xml.content"), + newOutputStream(configFile)); + + new DifferentiateBetweenConfigAndConfigEntryUpdateStep() {}.updateAllInDirectory(temp); + + assertThat(Files.readAllLines(configFile)) + .areAtLeastOne(new Condition<>(line -> line.contains(""), "line containing start tag with attribute")) + .endsWith(Files.readAllLines(Paths.get(getResource("sonia/scm/update/store/config_entry_file.xml.content").getFile())).toArray(new String[9])); + } + + private void assertContent(Path configFile, String expectedContentResource) { + assertThat(configFile) + .hasSameTextualContentAs(Paths.get(getResource(expectedContentResource).getFile())); + } +} diff --git a/scm-webapp/src/test/resources/sonia/scm/update/store/config_entry_file.xml.content b/scm-webapp/src/test/resources/sonia/scm/update/store/config_entry_file.xml.content new file mode 100644 index 0000000000..06fae8ab6b --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/update/store/config_entry_file.xml.content @@ -0,0 +1,13 @@ + + sonia.scm.pullrequest.comment.data.xml + + 2.0.2 + + + + sonia.scm.plugin.svn + + 2.0.0 + + + diff --git a/scm-webapp/src/test/resources/sonia/scm/update/store/config_entry_file_without_mark.xml.content b/scm-webapp/src/test/resources/sonia/scm/update/store/config_entry_file_without_mark.xml.content new file mode 100644 index 0000000000..e67a76a3a0 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/update/store/config_entry_file_without_mark.xml.content @@ -0,0 +1,15 @@ + + + + sonia.scm.pullrequest.comment.data.xml + + 2.0.2 + + + + sonia.scm.plugin.svn + + 2.0.0 + + + diff --git a/scm-webapp/src/test/resources/sonia/scm/update/store/config_file.xml.content b/scm-webapp/src/test/resources/sonia/scm/update/store/config_file.xml.content new file mode 100644 index 0000000000..e22489ad62 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/update/store/config_file.xml.content @@ -0,0 +1,11 @@ + + + false + false + false + false + false + scmadmin + {enc}v2:7iwd7q8K8i8sU2VB_ZzfCrJj_KMZPCCGLk82zVGA6PKp860Wou1VDA== + http://localhost:8080/jenkins/1 +