From 44d99fcc9d8ff093dccfd55453baddeb35ff9ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 27 Aug 2019 16:57:10 +0200 Subject: [PATCH] Create new modification command --- .../api/ModificationCommandBuilder.java | 194 ++++++++++++++++++ .../repository/spi/ModificationCommand.java | 15 ++ .../api/ModificationCommandBuilderTest.java | 160 +++++++++++++++ 3 files changed, 369 insertions(+) create mode 100644 scm-core/src/main/java/sonia/scm/repository/api/ModificationCommandBuilder.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/spi/ModificationCommand.java create mode 100644 scm-core/src/test/java/sonia/scm/repository/api/ModificationCommandBuilderTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModificationCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModificationCommandBuilder.java new file mode 100644 index 0000000000..863fb1731b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModificationCommandBuilder.java @@ -0,0 +1,194 @@ +package sonia.scm.repository.api; + +import com.google.common.io.ByteSource; +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; +import sonia.scm.repository.spi.ModificationCommand; +import sonia.scm.repository.util.WorkdirProvider; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class ModificationCommandBuilder { + + private final ModificationCommand command; + private final File workdir; + + private final List modifications = new ArrayList<>(); + + ModificationCommandBuilder(ModificationCommand command, WorkdirProvider workdirProvider) { + this.command = command; + this.workdir = workdirProvider.createNewWorkdir(); + } + + ModificationCommandBuilder setBranchToModify(String branchToModify) { + return this; + } + + ContentLoader createFile(String path) { + return new ContentLoader( + content -> modifications.add(new CreateFile(path, content)) + ); + } + + ContentLoader modifyFile(String path) { + return new ContentLoader( + content -> modifications.add(new ModifyFile(path, content)) + ); + } + + ModificationCommandBuilder deleteFile(String path) { + modifications.add(new DeleteFile(path)); + return this; + } + + ModificationCommandBuilder moveFile(String sourcePath, String targetPath) { + modifications.add(new MoveFile(sourcePath, targetPath)); + return this; + } + + String execute() { + modifications.forEach(FileModification::execute); + modifications.forEach(FileModification::cleanup); + return command.commit(); + } + + private Content loadData(ByteSource data) throws IOException { + File file = createTemporaryFile(); + data.copyTo(Files.asByteSink(file)); + return new Content(file); + } + + private Content loadData(InputStream data) throws IOException { + File file = createTemporaryFile(); + OutputStream out = Files.asByteSink(file).openBufferedStream(); + ByteStreams.copy(data, out); + out.close(); + return new Content(file); + } + + private File createTemporaryFile() throws IOException { + return File.createTempFile("upload-", "", workdir); + } + + private interface FileModification { + void execute(); + + default void cleanup() { + } + } + + private class DeleteFile implements FileModification { + private final String path; + + public DeleteFile(String path) { + this.path = path; + } + + @Override + public void execute() { + command.delete(path); + } + } + + private class MoveFile implements FileModification { + private final String sourcePath; + private final String targetPath; + + private MoveFile(String sourcePath, String targetPath) { + this.sourcePath = sourcePath; + this.targetPath = targetPath; + } + + @Override + public void execute() { + command.move(sourcePath, targetPath); + } + } + + private abstract class DataBasedModification implements FileModification { + + private final Content content; + + DataBasedModification(Content content) { + this.content = content; + } + + public Content getContent() { + return content; + } + @Override + public void cleanup() { + content.deleteFile(); + } + } + + private class CreateFile extends DataBasedModification { + + private final String path; + + CreateFile(String path, Content content) { + super(content); + this.path = path; + } + @Override + public void execute() { + command.create(path, getContent().getFile()); + } + } + + private class ModifyFile extends DataBasedModification { + + private final String path; + + ModifyFile(String path, Content content) { + super(content); + this.path = path; + } + @Override + public void execute() { + command.modify(path, getContent().getFile()); + } + } + + public class ContentLoader { + + private final Consumer contentConsumer; + + private ContentLoader(Consumer contentConsumer) { + this.contentConsumer = contentConsumer; + } + + public ModificationCommandBuilder withData(ByteSource data) throws IOException { + Content content = loadData(data); + contentConsumer.accept(content); + return ModificationCommandBuilder.this; + } + public ModificationCommandBuilder withData(InputStream data) throws IOException { + Content content = loadData(data); + contentConsumer.accept(content); + return ModificationCommandBuilder.this; + } + } + + private class Content { + + private final File file; + + Content(File file) { + this.file = file; + } + + private File getFile() { + return file; + } + public void deleteFile() { + file.delete(); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationCommand.java new file mode 100644 index 0000000000..c221acc1d6 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationCommand.java @@ -0,0 +1,15 @@ +package sonia.scm.repository.spi; + +import java.io.File; + +public interface ModificationCommand { + String commit(); + + void delete(String toBeDeleted); + + void create(String toBeCreated, File file); + + void modify(String path, File file); + + void move(String sourcePath, String targetPath); +} diff --git a/scm-core/src/test/java/sonia/scm/repository/api/ModificationCommandBuilderTest.java b/scm-core/src/test/java/sonia/scm/repository/api/ModificationCommandBuilderTest.java new file mode 100644 index 0000000000..f27b89964c --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/api/ModificationCommandBuilderTest.java @@ -0,0 +1,160 @@ +package sonia.scm.repository.api; + +import com.google.common.io.ByteSource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; +import sonia.scm.repository.spi.ModificationCommand; +import sonia.scm.repository.util.WorkdirProvider; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) +class ModificationCommandBuilderTest { + + @Mock + ModificationCommand command; + @Mock + WorkdirProvider workdirProvider; + + ModificationCommandBuilder commandBuilder; + + @BeforeEach + void initWorkdir(@TempDirectory.TempDir Path temp) throws IOException { + lenient().when(workdirProvider.createNewWorkdir()).thenReturn(temp.toFile()); + commandBuilder = new ModificationCommandBuilder(command, workdirProvider); + } + + @Test + void shouldReturnTargetRevisionFromCommit() { + when(command.commit()).thenReturn("target"); + + String targetRevision = commandBuilder.execute(); + + assertThat(targetRevision).isEqualTo("target"); + } + + @Test + void shouldExecuteDelete() { + commandBuilder + .deleteFile("toBeDeleted") + .execute(); + + verify(command).delete("toBeDeleted"); + } + + @Test + void shouldExecuteMove() { + commandBuilder + .moveFile("source", "target") + .execute(); + + verify(command).move("source", "target"); + } + + @Test + void shouldExecuteCreateWithByteSourceContent() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(command).create(nameCaptor.capture(), any()); + + commandBuilder + .createFile("toBeCreated").withData(ByteSource.wrap("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldExecuteCreateWithInputStreamContent() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(command).create(nameCaptor.capture(), any()); + + commandBuilder + .createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldExecuteCreateMultipleTimes() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(command).create(nameCaptor.capture(), any()); + + commandBuilder + .createFile("toBeCreated_1").withData(new ByteArrayInputStream("content_1".getBytes())) + .createFile("toBeCreated_2").withData(new ByteArrayInputStream("content_2".getBytes())) + .execute(); + + List createdNames = nameCaptor.getAllValues(); + assertThat(createdNames.get(0)).isEqualTo("toBeCreated_1"); + assertThat(createdNames.get(1)).isEqualTo("toBeCreated_2"); + assertThat(contentCaptor).contains("content_1", "content_2"); + } + + @Test + void shouldExecuteModify() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(command).modify(nameCaptor.capture(), any()); + + commandBuilder + .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeModified"); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldDeleteTemporaryFiles(@TempDirectory.TempDir Path temp) throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); + doNothing().when(command).modify(nameCaptor.capture(), fileCaptor.capture()); + + commandBuilder + .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) + .execute(); + + assertThat(Files.list(temp)).isEmpty(); + } + + private static class ExtractContent implements Answer { + private final List contentCaptor; + + public ExtractContent(List contentCaptor) { + this.contentCaptor = contentCaptor; + } + + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return contentCaptor.add(Files.readAllLines(((File) invocation.getArgument(1)).toPath()).get(0)); + } + } +}