From ac03d290b10047b876e8f493f31486e924828da9 Mon Sep 17 00:00:00 2001 From: davinkevin Date: Tue, 25 Sep 2018 05:24:59 +0200 Subject: [PATCH] refactor(kotlin): migration of the DatabaseService to Kotlin --- README.md | 2 +- .../controller/task/DatabaseController.java | 3 +- .../scheduled/DatabaseScheduled.java | 2 +- .../service/DatabaseService.java | 84 ------------ .../service/properties/Backup.java | 15 ++- .../podcastserver/service/DatabaseService.kt | 77 +++++++++++ .../scheduled/DatabaseScheduledTest.java | 2 +- .../service/DatabaseServiceTest.java | 117 ----------------- .../service/DatabaseServiceTest.kt | 124 ++++++++++++++++++ 9 files changed, 218 insertions(+), 208 deletions(-) delete mode 100755 backend/src/main/java/lan/dk/podcastserver/service/DatabaseService.java create mode 100755 backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/DatabaseService.kt delete mode 100644 backend/src/test/java/lan/dk/podcastserver/service/DatabaseServiceTest.java create mode 100644 backend/src/test/kotlin/com/github/davinkevin/podcastserver/service/DatabaseServiceTest.kt diff --git a/README.md b/README.md index 1fac3f2ad..539761aeb 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Podcast-Server Back-end : [![Codacy Badge](https://api.codacy.com/project/badge/Grade/2030290b1c2145f6878e9ad7811c542e)](https://www.codacy.com/app/davin-kevin/Podcast-Server?utm_source=github.com&utm_medium=referral&utm_content=davinkevin/Podcast-Server&utm_campaign=Badge_Grade) [![Coverage Status](https://coveralls.io/repos/davinkevin/Podcast-Server/badge.svg?branch=master)](https://coveralls.io/r/davinkevin/Podcast-Server?branch=master) -Migration : ![Java stage](https://badgen.net/badge/Java/90%25/orange) ![Kotlin stage](https://badgen.net/badge/Kotlin/10%25/purple) +Migration : ![Java stage](https://badgen.net/badge/Java/89%25/orange) ![Kotlin stage](https://badgen.net/badge/Kotlin/11%25/purple) Front-end : [![Code Climate](https://codeclimate.com/github/davinkevin/Podcast-Server/badges/gpa.svg)](https://codeclimate.com/github/davinkevin/Podcast-Server) diff --git a/backend/src/main/java/lan/dk/podcastserver/controller/task/DatabaseController.java b/backend/src/main/java/lan/dk/podcastserver/controller/task/DatabaseController.java index 33bfcab06..df0361f22 100755 --- a/backend/src/main/java/lan/dk/podcastserver/controller/task/DatabaseController.java +++ b/backend/src/main/java/lan/dk/podcastserver/controller/task/DatabaseController.java @@ -1,8 +1,7 @@ package lan.dk.podcastserver.controller.task; -import lan.dk.podcastserver.service.DatabaseService; +import com.github.davinkevin.podcastserver.service.DatabaseService; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; diff --git a/backend/src/main/java/lan/dk/podcastserver/scheduled/DatabaseScheduled.java b/backend/src/main/java/lan/dk/podcastserver/scheduled/DatabaseScheduled.java index c4124fafc..5a45ff07f 100644 --- a/backend/src/main/java/lan/dk/podcastserver/scheduled/DatabaseScheduled.java +++ b/backend/src/main/java/lan/dk/podcastserver/scheduled/DatabaseScheduled.java @@ -1,6 +1,6 @@ package lan.dk.podcastserver.scheduled; -import lan.dk.podcastserver.service.DatabaseService; +import com.github.davinkevin.podcastserver.service.DatabaseService; import lombok.RequiredArgsConstructor; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.annotation.Scheduled; diff --git a/backend/src/main/java/lan/dk/podcastserver/service/DatabaseService.java b/backend/src/main/java/lan/dk/podcastserver/service/DatabaseService.java deleted file mode 100755 index a23b99825..000000000 --- a/backend/src/main/java/lan/dk/podcastserver/service/DatabaseService.java +++ /dev/null @@ -1,84 +0,0 @@ -package lan.dk.podcastserver.service; - -import lan.dk.podcastserver.service.properties.Backup; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.rauschig.jarchivelib.ArchiveFormat; -import org.rauschig.jarchivelib.Archiver; -import org.rauschig.jarchivelib.ArchiverFactory; -import org.rauschig.jarchivelib.CompressionType; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import javax.persistence.EntityManager; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; - -import static java.time.temporal.ChronoField.*; - -/** - * Created by kevin on 28/03/2016 for Podcast Server - */ -@Slf4j -@Service -@RequiredArgsConstructor -@ConditionalOnProperty("podcastserver.backup.enabled") -public class DatabaseService { - - private static final Archiver archiver = ArchiverFactory.createArchiver(ArchiveFormat.TAR, CompressionType.GZIP); - private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder() - .appendValue(YEAR, 4) - .appendLiteral("-") - .appendValue(MONTH_OF_YEAR, 2) - .appendLiteral("-") - .appendValue(DAY_OF_MONTH, 2) - .appendLiteral("-") - .appendValue(HOUR_OF_DAY, 2) - .appendLiteral("-") - .appendValue(MINUTE_OF_HOUR, 2) - .toFormatter(); - - private static final String QUERY_BACKUP_SQL = "SCRIPT TO '%s'"; - private static final String QUERY_BACKUP_BINARY = "BACKUP TO '%s'"; - - private final Backup backup; - private final EntityManager em; - - @Transactional - public Path backupWithDefault() throws IOException { - log.info("Doing backup operation"); - Path result = backup(this.backup.getLocation(), this.backup.getBinary()); - log.info("End of backup operation"); - return result; - } - - @Transactional - public Path backup(Path destinationDirectory, Boolean isBinary) throws IOException { - - if (!Files.isDirectory(destinationDirectory)) { - log.error("The path {} is not a directory, can't be use for backup", destinationDirectory.toString()); - return destinationDirectory; - } - - Path backupFile = destinationDirectory.toAbsolutePath().resolve("podcast-server-" + ZonedDateTime.now().format(formatter) + (isBinary ? "" : ".sql")); - - // Simpler way to execute query via JPA, ExecuteUpdate not allowed here - em.createNativeQuery(generateQuery(isBinary, backupFile)).getResultList(); - - - archiver.create(backupFile.getFileName().toString(), backupFile.getParent().toFile(), backupFile.toFile()); - Files.deleteIfExists(backupFile); - - return backupFile.resolveSibling(backupFile.getFileName() + ".tar.gz"); - } - - private String generateQuery(Boolean isBinary, Path backupFile) { - return String.format(isBinary ? QUERY_BACKUP_BINARY : QUERY_BACKUP_SQL,backupFile.toString()); - } -} - diff --git a/backend/src/main/java/lan/dk/podcastserver/service/properties/Backup.java b/backend/src/main/java/lan/dk/podcastserver/service/properties/Backup.java index 0ae55be69..1cc3c5327 100644 --- a/backend/src/main/java/lan/dk/podcastserver/service/properties/Backup.java +++ b/backend/src/main/java/lan/dk/podcastserver/service/properties/Backup.java @@ -1,6 +1,5 @@ package lan.dk.podcastserver.service.properties; -import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -10,11 +9,23 @@ /** * Created by kevin on 12/04/2016 for Podcast Server */ -@Getter @Setter +@Setter @Accessors(chain = true) @ConfigurationProperties("podcastserver.backup") public class Backup { private Path location; private Boolean binary = false; private String cron = "0 0 4 * * *"; + + public Path getLocation() { + return this.location; + } + + public Boolean getBinary() { + return this.binary; + } + + public String getCron() { + return this.cron; + } } diff --git a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/DatabaseService.kt b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/DatabaseService.kt new file mode 100755 index 000000000..19452e9f8 --- /dev/null +++ b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/DatabaseService.kt @@ -0,0 +1,77 @@ +package com.github.davinkevin.podcastserver.service + +import lan.dk.podcastserver.service.properties.Backup +import org.rauschig.jarchivelib.ArchiveFormat +import org.rauschig.jarchivelib.ArchiverFactory +import org.rauschig.jarchivelib.CompressionType +import org.slf4j.LoggerFactory +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatterBuilder +import java.time.temporal.ChronoField.* +import javax.persistence.EntityManager + +/** + * Created by kevin on 28/03/2016 for Podcast Server + */ +@Service +@ConditionalOnProperty("podcastserver.backup.enabled") +class DatabaseService(val backup: Backup, val em: EntityManager) { + + private val log = LoggerFactory.getLogger(this.javaClass.name)!! + + @Transactional + fun backupWithDefault(): Path { + log.info("Doing backup operation") + val result = backup(this.backup.location, this.backup.binary) + log.info("End of backup operation") + return result + } + + @Transactional + @Throws(IOException::class) + fun backup(destinationDirectory: Path, isBinary: Boolean = false): Path { + + if (!Files.isDirectory(destinationDirectory)) { + log.error("The path {} is not a directory, can't be use for backup", destinationDirectory.toString()) + return destinationDirectory + } + + val backupFile = destinationDirectory.toAbsolutePath().resolve("podcast-server-" + ZonedDateTime.now().format(formatter) + if (isBinary) "" else ".sql") + + // Simpler way to execute query via JPA, ExecuteUpdate not allowed here + em.createNativeQuery(generateQuery(isBinary, backupFile)).resultList + + + archiver.create(backupFile.fileName.toString(), backupFile.parent.toFile(), backupFile.toFile()) + Files.deleteIfExists(backupFile) + + return backupFile.resolveSibling("${backupFile.fileName}.tar.gz") + } + + private fun generateQuery(isBinary: Boolean, backupFile: Path) = + if (isBinary) "BACKUP TO '$backupFile'" + else "SCRIPT TO '$backupFile'" + + companion object { + + private val archiver = ArchiverFactory.createArchiver(ArchiveFormat.TAR, CompressionType.GZIP) + private val formatter = DateTimeFormatterBuilder() + .appendValue(YEAR, 4) + .appendLiteral("-") + .appendValue(MONTH_OF_YEAR, 2) + .appendLiteral("-") + .appendValue(DAY_OF_MONTH, 2) + .appendLiteral("-") + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral("-") + .appendValue(MINUTE_OF_HOUR, 2) + .toFormatter() + } +} + diff --git a/backend/src/test/java/lan/dk/podcastserver/scheduled/DatabaseScheduledTest.java b/backend/src/test/java/lan/dk/podcastserver/scheduled/DatabaseScheduledTest.java index d1b2bbb9b..85e329d58 100644 --- a/backend/src/test/java/lan/dk/podcastserver/scheduled/DatabaseScheduledTest.java +++ b/backend/src/test/java/lan/dk/podcastserver/scheduled/DatabaseScheduledTest.java @@ -1,6 +1,6 @@ package lan.dk.podcastserver.scheduled; -import lan.dk.podcastserver.service.DatabaseService; +import com.github.davinkevin.podcastserver.service.DatabaseService; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; diff --git a/backend/src/test/java/lan/dk/podcastserver/service/DatabaseServiceTest.java b/backend/src/test/java/lan/dk/podcastserver/service/DatabaseServiceTest.java deleted file mode 100644 index 60491d69e..000000000 --- a/backend/src/test/java/lan/dk/podcastserver/service/DatabaseServiceTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package lan.dk.podcastserver.service; - -import lan.dk.podcastserver.service.properties.Backup; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.hibernate.search.jpa.FullTextEntityManager; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; - -import javax.persistence.Query; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -import static java.util.Objects.nonNull; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.*; - -/** - * Created by kevin on 29/03/2016 for Podcast Server - */ -@Slf4j -@RunWith(MockitoJUnitRunner.class) -public class DatabaseServiceTest { - - private static final Path NOT_DIRECTORY = Paths.get("/tmp", "foo.bar"); - - @Mock Backup backup; - @Mock FullTextEntityManager fem; - @InjectMocks DatabaseService databaseService; - - private Query query; - private Path backupToCreate; - - @Before - public void beforeEach() throws IOException { - Files.deleteIfExists(NOT_DIRECTORY); - if (nonNull(backupToCreate)) { - Files.deleteIfExists(backupToCreate); - } - //FileSystemUtils.deleteRecursively(Paths.get("/tmp", "foo.bar").toFile()); - } - - @Test - public void should_reject_if_destination_isnt_directory() throws IOException { - /* Given */ - Files.createFile(NOT_DIRECTORY); - - /* When */ - Path backupFile = databaseService.backup(NOT_DIRECTORY, true); - - /* Then */ - assertThat(backupFile).isSameAs(NOT_DIRECTORY); - verify(fem, never()).createNativeQuery(anyString()); - } - - @Test - public void should_generate_an_archive_of_db_in_binary() throws IOException { - /* Given */ - when(fem.createNativeQuery(anyString())).then(generateDumpFile()); - - /* When */ - Path backupFile = databaseService.backup(Paths.get("/tmp"), true); - - /* Then */ - verify(fem, times(1)).createNativeQuery(contains("BACKUP TO")); - assertThat(backupFile).exists().hasFileName(backupToCreate.getFileName() + ".tar.gz"); - } - - @Test - public void should_generate_an_archive_of_db_in_sql() throws IOException { - /* Given */ - when(fem.createNativeQuery(anyString())).then(generateDumpFile()); - - /* When */ - Path backupFile = databaseService.backup(Paths.get("/tmp"), false); - - /* Then */ - verify(fem, times(1)).createNativeQuery(contains("SCRIPT TO")); - assertThat(backupFile) - .exists() - .hasFileName(backupToCreate.getFileName() + ".tar.gz"); - } - - @Test - public void should_generate_from_backup_parameters() throws IOException { - /* Given */ - when(fem.createNativeQuery(anyString())).then(generateDumpFile()); - when(backup.getBinary()).thenReturn(Boolean.FALSE); - when(backup.getLocation()).thenReturn(Paths.get("/tmp")); - - /* When */ - Path backupFile = databaseService.backupWithDefault(); - - /* Then */ - verify(fem, times(1)).createNativeQuery(contains("SCRIPT TO")); - assertThat(backupFile) - .exists() - .hasFileName(backupToCreate.getFileName() + ".tar.gz"); - } - - private Answer generateDumpFile() { - return i -> { - backupToCreate = Paths.get(StringUtils.substringAfter(i.getArguments()[0].toString(), "\'").replace("'", "")); - Files.createFile(backupToCreate); - query = mock(Query.class); - return query; - }; - } -} diff --git a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/service/DatabaseServiceTest.kt b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/service/DatabaseServiceTest.kt new file mode 100644 index 000000000..88f9b34a0 --- /dev/null +++ b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/service/DatabaseServiceTest.kt @@ -0,0 +1,124 @@ +package com.github.davinkevin.podcastserver.service + +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.whenever +import lan.dk.podcastserver.service.properties.Backup +import org.assertj.core.api.Assertions.assertThat +import org.hibernate.search.jpa.FullTextEntityManager +import org.junit.jupiter.api.AfterEach +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.mockito.ArgumentMatchers.* +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.invocation.InvocationOnMock +import org.mockito.junit.jupiter.MockitoExtension +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import javax.persistence.Query + +/** + * Created by kevin on 29/03/2016 for Podcast Server + */ +@ExtendWith(MockitoExtension::class) +class DatabaseServiceTest { + + @Mock lateinit var backup: Backup + @Mock lateinit var fem: FullTextEntityManager + @InjectMocks lateinit var databaseService: DatabaseService + + private var query: Query? = null + private var backupToCreate: Path? = null + + @Nested + inner class ErrorCases { + + @BeforeEach @AfterEach + fun beforeEach() { + Files.deleteIfExists(NOT_DIRECTORY) + } + + @Test + fun `should reject if destination is not a directory`() { + /* Given */ + Files.createFile(NOT_DIRECTORY) + + /* When */ + val backupFile = databaseService.backup(NOT_DIRECTORY, true) + + /* Then */ + assertThat(backupFile).isSameAs(NOT_DIRECTORY) + verify(fem, never()).createNativeQuery(anyString()) + } + } + + @Nested + inner class BackupGeneration { + @AfterEach + fun afterEach() { + Files.deleteIfExists(backupToCreate) + } + + @Test + fun `should generate an archive of db in binary format`() { + /* Given */ + whenever(fem.createNativeQuery(anyString())).then { generateDumpFile(it) } + + /* When */ + val backupFile = databaseService.backup(Paths.get("/tmp"), true) + + /* Then */ + verify(fem, times(1)).createNativeQuery(contains("BACKUP TO")) + assertThat(backupFile).exists().hasFileName("${backupToCreate!!.fileName}.tar.gz") + } + + @Test + fun `should generate an archive of db in sql format`() { + /* Given */ + whenever(fem.createNativeQuery(startsWith("SCRIPT TO"))).then { generateDumpFile(it) } + + /* When */ + val backupFile = databaseService.backup(Paths.get("/tmp"), false) + + /* Then */ + verify(fem, times(1)).createNativeQuery(contains("SCRIPT TO")) + assertThat(backupFile) + .exists() + .hasFileName("${backupToCreate!!.fileName}.tar.gz") + } + + @Test + fun `should generate from backup parameters`() { + /* Given */ + whenever(backup.binary).thenReturn(false) + whenever(backup.location).thenReturn(Paths.get("/tmp")) + whenever(fem.createNativeQuery(startsWith("SCRIPT TO"))).then { generateDumpFile(it) } + + /* When */ + val backupFile = databaseService.backupWithDefault() + + /* Then */ + verify(fem, times(1)).createNativeQuery(contains("SCRIPT TO")) + assertThat(backupFile) + .exists() + .hasFileName("${backupToCreate!!.fileName}.tar.gz") + } + + private fun generateDumpFile(i: InvocationOnMock): Query? { + backupToCreate = Paths.get(i.arguments[0].toString().substringAfter("\'").replace("'", "")) + Files.createFile(backupToCreate) + query = mock() + return query + } + } + + companion object { + private val NOT_DIRECTORY = Paths.get("/tmp", "foo.bar") + } +}