From 865a550ebdbbe26bede0704dcb88fed2e81e873a Mon Sep 17 00:00:00 2001 From: Eugen Mayer <136934+EugenMayer@users.noreply.github.com> Date: Mon, 30 Jan 2023 08:48:45 +0100 Subject: [PATCH] Reimplement with Kotlin (#70) Re-implement in Kotlin Also - restructure package - better sanitize filename - use proper webmvc test - cleanup slices - fix code-styles --- README.md | 3 +- build.gradle | 84 +++-------- examples/example.docx | Bin 4263 -> 0 bytes gradle/configuration.gradle | 24 +++ gradle/dependencies.gradle | 51 +++++++ gradle/kotlin.gradle | 19 +++ .../converter/ConverterApplication.java | 12 -- .../converter/service/ConverterService.java | 56 ------- .../service/api/UnknownSourceFormat.java | 33 ---- .../converter/web/ConversionController.java | 66 -------- .../converter/ConverterApplication.kt | 14 ++ .../converter/base/FileNameUtils.kt | 12 ++ .../module/convert/ConverterService.kt | 46 ++++++ .../api/UnknownSourceFormatException.kt | 3 + .../controller/ConversionController.kt | 66 ++++++++ .../testingUtils/profiles/SetupE2eTest.java | 22 --- .../testingUtils/profiles/SetupItTest.java | 23 --- .../testingUtils/profiles/SetupUnitTest.java | 18 --- .../slices/CustomSpringBootTestSlice.java | 17 --- .../slices/CustomWebMvcWithJpaTestSlice.java | 27 ---- .../web/ConversionControllerTest.java | 133 ----------------- .../converter/base/FileNameUtilsTest.kt | 27 ++++ .../controller/ConversionControllerTest.kt | 141 ++++++++++++++++++ .../testingUtils/profiles/SetupE2eTest.kt | 21 +++ .../testingUtils/profiles/SetupItTest.kt | 21 +++ .../testingUtils/profiles/SetupUnitTest.kt | 15 ++ .../slices/CustomSpringBootTestSlice.kt | 11 ++ .../testingUtils/slices/CustomWebMvcSlice.kt | 21 +++ 28 files changed, 511 insertions(+), 475 deletions(-) delete mode 100644 examples/example.docx create mode 100644 gradle/configuration.gradle create mode 100644 gradle/dependencies.gradle create mode 100644 gradle/kotlin.gradle delete mode 100644 src/main/java/de/kontextwork/converter/ConverterApplication.java delete mode 100644 src/main/java/de/kontextwork/converter/service/ConverterService.java delete mode 100644 src/main/java/de/kontextwork/converter/service/api/UnknownSourceFormat.java delete mode 100644 src/main/java/de/kontextwork/converter/web/ConversionController.java create mode 100644 src/main/kotlin/de/kontextwork/converter/ConverterApplication.kt create mode 100644 src/main/kotlin/de/kontextwork/converter/base/FileNameUtils.kt create mode 100644 src/main/kotlin/de/kontextwork/converter/module/convert/ConverterService.kt create mode 100644 src/main/kotlin/de/kontextwork/converter/module/convert/api/UnknownSourceFormatException.kt create mode 100644 src/main/kotlin/de/kontextwork/converter/module/convert/controller/ConversionController.kt delete mode 100644 src/test/java/de/kontextwork/converter/testingUtils/profiles/SetupE2eTest.java delete mode 100644 src/test/java/de/kontextwork/converter/testingUtils/profiles/SetupItTest.java delete mode 100644 src/test/java/de/kontextwork/converter/testingUtils/profiles/SetupUnitTest.java delete mode 100644 src/test/java/de/kontextwork/converter/testingUtils/slices/CustomSpringBootTestSlice.java delete mode 100644 src/test/java/de/kontextwork/converter/testingUtils/slices/CustomWebMvcWithJpaTestSlice.java delete mode 100644 src/test/java/de/kontextwork/converter/web/ConversionControllerTest.java create mode 100644 src/test/kotlin/de/kontextwork/converter/base/FileNameUtilsTest.kt create mode 100644 src/test/kotlin/de/kontextwork/converter/module/convert/controller/ConversionControllerTest.kt create mode 100644 src/test/kotlin/de/kontextwork/converter/testingUtils/profiles/SetupE2eTest.kt create mode 100644 src/test/kotlin/de/kontextwork/converter/testingUtils/profiles/SetupItTest.kt create mode 100644 src/test/kotlin/de/kontextwork/converter/testingUtils/profiles/SetupUnitTest.kt create mode 100644 src/test/kotlin/de/kontextwork/converter/testingUtils/slices/CustomSpringBootTestSlice.kt create mode 100644 src/test/kotlin/de/kontextwork/converter/testingUtils/slices/CustomWebMvcSlice.kt diff --git a/README.md b/README.md index 9059068..b89bb5c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ docker run --memory 512m --name converter-prod --rm -p 8080:8080 ghcr.io/eugenma Now convert a `docx` to `html` ```bash cd officeconverter -curl -F file=@examples/example.docx "localhost:14080/conversion?format=html" -o /tmp/test.html +curl -F file=@src/test/resources/testfiles/withpictures.docx "localhost:14080/conversion?format=html" -o /tmp/test.html +curl -F file=@src/test/resources/testfiles/template.dotx "localhost:14080/conversion?format=html" -o /tmp/test.html ``` ## Build diff --git a/build.gradle b/build.gradle index 631525e..471d3e7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,5 @@ buildscript { + ext.kotlin_version = '1.8.0' ext { // @see https://mvnrepository.com/artifact/org.jodconverter/jodconverter-local jodconverterVersion = '4.4.6' @@ -22,6 +23,14 @@ buildscript { // @see https://mvnrepository.com/artifact/org.glassfish.jaxb/jaxb-runtime jaxb = "4.0.1" } + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } + + ext['log4j2.version'] = '2.19.0' } plugins { @@ -41,85 +50,26 @@ plugins { // @see https://plugins.gradle.org/plugin/org.sonarqube id "org.sonarqube" version "3.5.0.2730" + + id "org.jetbrains.kotlin.jvm" version "1.8.0" + id "org.jetbrains.kotlin.plugin.allopen" version "1.8.0" + id "org.jetbrains.kotlin.plugin.spring" version "1.8.0" } apply plugin: 'io.spring.dependency-management' +apply plugin: 'kotlin' apply plugin: 'org.springframework.boot' apply from: 'gradle/repositories.gradle' +apply from: 'gradle/dependencies.gradle' apply from: 'gradle/build.gradle' apply from: 'gradle/tests.gradle' apply from: 'gradle/spring_bootRun.gradle' apply from: 'gradle/sonarqube.gradle' +apply from: 'gradle/kotlin.gradle' +apply from: 'gradle/configuration.gradle' -configurations { - compileOnly { - extendsFrom annotationProcessor - } - - developmentOnly - - providedRuntime - - // needed to get lombok working in our tests - testImplementation { - extendsFrom annotationProcessor - } -} group = 'de.kontextwork' // this lets us set the version during build using cli -Pversion=1.1.1 version = "${version}" -sourceCompatibility = 17 -targetCompatibility = 17 -tasks.withType(JavaCompile) { - options.encoding = 'UTF-8' -} - -ext['log4j2.version'] = '2.19.0' - -// @see https://github.com/sbrannen/spring-events/blob/master/build.gradle#L38 -// and @see https://stackoverflow.com/a/54605523/3625317 -dependencies { - implementation( - "org.jodconverter:jodconverter-local:$jodconverterVersion", - "org.jodconverter:jodconverter-spring-boot-starter:$jodconverterVersion", - "org.springframework.boot:spring-boot-starter-web", - "org.springframework:spring-core", - "commons-io:commons-io:$commonsIo", - - // needed when compiling against > Java 8 since jaxb is no longer included - // you would get Error creating bean with name 'xmlModelPlugin': Lookup method resolution failed - "org.glassfish.jaxb:jaxb-runtime:$jaxb" - ) - - testImplementation( - "org.springframework.boot:spring-boot-starter-test", - "org.junit.jupiter:junit-jupiter-api", - "org.junit.jupiter:junit-jupiter-params", - "org.mockito:mockito-core:$mockitoVersion", - "org.mockito:mockito-junit-jupiter:$mockitoVersion", - "org.apache.tika:tika-core:${tikaVersion}", - "org.apache.tika:tika-parsers:${tikaVersion}", - ) - - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") - - annotationProcessor( - "javax.annotation:javax.annotation-api:$javaxAnnotations", - "org.projectlombok:lombok:$lombokVersion" - ) - - testAnnotationProcessor( - "org.projectlombok:lombok:$lombokVersion", - ) - - compileOnly( - "org.projectlombok:lombok:$lombokVersion", - "org.springframework.boot:spring-boot-configuration-processor" - ) - - developmentOnly("org.springframework.boot:spring-boot-devtools") - - providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' -} diff --git a/examples/example.docx b/examples/example.docx deleted file mode 100644 index 2cdb5f353a511dc9e2eda6183de81a8d724924ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4263 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 Java 8 since jaxb is no longer included + // you would get Error creating bean with name 'xmlModelPlugin': Lookup method resolution failed + "org.glassfish.jaxb:jaxb-runtime:$jaxb", + + "org.jetbrains.kotlin:kotlin-reflect", + "org.jetbrains.kotlin:kotlin-stdlib-jdk8", + 'org.apache.logging.log4j:log4j-api-kotlin:1.2.0' + ) + + testImplementation( + "org.springframework.boot:spring-boot-starter-test", + "org.junit.jupiter:junit-jupiter-api", + "org.junit.jupiter:junit-jupiter-params", + "org.mockito:mockito-core:$mockitoVersion", + "org.mockito:mockito-junit-jupiter:$mockitoVersion", + "org.apache.tika:tika-core:${tikaVersion}", + "org.apache.tika:tika-parsers:${tikaVersion}", + ) + + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + + annotationProcessor( + "javax.annotation:javax.annotation-api:$javaxAnnotations", + "org.projectlombok:lombok:$lombokVersion" + ) + + testAnnotationProcessor( + "org.projectlombok:lombok:$lombokVersion", + ) + + compileOnly( + "org.projectlombok:lombok:$lombokVersion", + "org.springframework.boot:spring-boot-configuration-processor" + ) + + developmentOnly("org.springframework.boot:spring-boot-devtools") + + runtimeOnly 'org.springframework.boot:spring-boot-starter-tomcat' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" +} diff --git a/gradle/kotlin.gradle b/gradle/kotlin.gradle new file mode 100644 index 0000000..0ed90e1 --- /dev/null +++ b/gradle/kotlin.gradle @@ -0,0 +1,19 @@ +kotlin { + // see https://docs.gradle.org/current/userguide/toolchains.html + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +compileKotlin { + compilerOptions { + freeCompilerArgs = ["-Xjsr305=strict"] + } +} + +compileTestKotlin { + compilerOptions { + freeCompilerArgs = ["-Xjsr305=strict"] + } +} + diff --git a/src/main/java/de/kontextwork/converter/ConverterApplication.java b/src/main/java/de/kontextwork/converter/ConverterApplication.java deleted file mode 100644 index 51fe830..0000000 --- a/src/main/java/de/kontextwork/converter/ConverterApplication.java +++ /dev/null @@ -1,12 +0,0 @@ -package de.kontextwork.converter; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class ConverterApplication { - - public static void main(String[] args) { - SpringApplication.run(ConverterApplication.class, args); - } -} diff --git a/src/main/java/de/kontextwork/converter/service/ConverterService.java b/src/main/java/de/kontextwork/converter/service/ConverterService.java deleted file mode 100644 index 9612f1f..0000000 --- a/src/main/java/de/kontextwork/converter/service/ConverterService.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.kontextwork.converter.service; - -import de.kontextwork.converter.service.api.UnknownSourceFormat; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import lombok.RequiredArgsConstructor; -import org.apache.commons.io.FilenameUtils; -import org.jodconverter.core.DocumentConverter; -import org.jodconverter.core.document.DefaultDocumentFormatRegistry; -import org.jodconverter.core.document.DocumentFormat; -import org.jodconverter.core.office.OfficeException; -import org.jodconverter.core.office.OfficeManager; -import org.jodconverter.local.LocalConverter; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class ConverterService -{ - private final OfficeManager officeManager; - - public ByteArrayOutputStream doConvert( - final DocumentFormat targetFormat, - final InputStream inputFile, - String inputFileName - ) throws UnknownSourceFormat, OfficeException - { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - - final DocumentConverter converter = LocalConverter.builder() - .officeManager(officeManager) - .build(); - - final DocumentFormat sourceFormat = DefaultDocumentFormatRegistry.getFormatByExtension( - FilenameUtils.getExtension(inputFileName) - ); - - if (sourceFormat == null) { - throw new UnknownSourceFormat( - String.format( - "Cannot convert file with extension %s since we cannot find the format in our registry", - FilenameUtils.getExtension(inputFileName) - ) - ); - } - - // Convert... - converter.convert(inputFile) - .as(sourceFormat) - .to(outputStream) - .as(targetFormat) - .execute(); - - return outputStream; - } -} diff --git a/src/main/java/de/kontextwork/converter/service/api/UnknownSourceFormat.java b/src/main/java/de/kontextwork/converter/service/api/UnknownSourceFormat.java deleted file mode 100644 index 6038f7d..0000000 --- a/src/main/java/de/kontextwork/converter/service/api/UnknownSourceFormat.java +++ /dev/null @@ -1,33 +0,0 @@ -package de.kontextwork.converter.service.api; - -public class UnknownSourceFormat extends Exception -{ - public UnknownSourceFormat() - { - } - - public UnknownSourceFormat(final String message) - { - super(message); - } - - public UnknownSourceFormat(final String message, final Throwable cause) - { - super(message, cause); - } - - public UnknownSourceFormat(final Throwable cause) - { - super(cause); - } - - public UnknownSourceFormat( - final String message, - final Throwable cause, - final boolean enableSuppression, - final boolean writableStackTrace - ) - { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/src/main/java/de/kontextwork/converter/web/ConversionController.java b/src/main/java/de/kontextwork/converter/web/ConversionController.java deleted file mode 100644 index 8a3bf8b..0000000 --- a/src/main/java/de/kontextwork/converter/web/ConversionController.java +++ /dev/null @@ -1,66 +0,0 @@ -package de.kontextwork.converter.web; - -import de.kontextwork.converter.service.ConverterService; -import de.kontextwork.converter.service.api.UnknownSourceFormat; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import lombok.extern.log4j.Log4j2; -import org.apache.commons.io.FilenameUtils; -import org.jodconverter.core.document.DefaultDocumentFormatRegistry; -import org.jodconverter.core.document.DocumentFormat; -import org.jodconverter.core.office.OfficeException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.*; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -@SuppressWarnings({"SpringJavaAutowiredFieldsWarningInspection", "unused"}) -@Log4j2 -@RestController -@RequestMapping("/conversion") -public class ConversionController -{ - @Autowired - private ConverterService converterService; - - @PostMapping(path = "") - @SuppressWarnings("java:S1452") - public ResponseEntity convert( - @RequestParam(name = "format", defaultValue = "pdf") final String targetFormatExt, - @RequestParam("file") final MultipartFile inputMultipartFile - ) throws IOException - { - final DocumentFormat conversionTargetFormat = DefaultDocumentFormatRegistry.getFormatByExtension(targetFormatExt); - - if (conversionTargetFormat == null) { - log.error(String.format("Unknown conversion target %s", targetFormatExt)); - return ResponseEntity.status(HttpStatus.PRECONDITION_FAILED).build(); - } - - ByteArrayOutputStream convertedFile; - try { - convertedFile = converterService.doConvert( - conversionTargetFormat, - inputMultipartFile.getInputStream(), - inputMultipartFile.getOriginalFilename() - ); - } catch (UnknownSourceFormat unknownSourceFormat) { - log.error(unknownSourceFormat.getMessage()); - return ResponseEntity.status(HttpStatus.PRECONDITION_FAILED).body(unknownSourceFormat.getMessage()); - } catch (OfficeException officeException) { - log.error(officeException.getMessage()); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - - final HttpHeaders headers = new HttpHeaders(); - String targetFilename = String.format( - "%s.%s", - FilenameUtils.getBaseName(inputMultipartFile.getOriginalFilename()), - conversionTargetFormat.getExtension() - ); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + targetFilename); - headers.setContentType(MediaType.parseMediaType(conversionTargetFormat.getMediaType())); - return ResponseEntity.ok().headers(headers).body(convertedFile.toByteArray()); - } -} - diff --git a/src/main/kotlin/de/kontextwork/converter/ConverterApplication.kt b/src/main/kotlin/de/kontextwork/converter/ConverterApplication.kt new file mode 100644 index 0000000..fb44f53 --- /dev/null +++ b/src/main/kotlin/de/kontextwork/converter/ConverterApplication.kt @@ -0,0 +1,14 @@ +package de.kontextwork.converter + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication + +@SpringBootApplication +class ConverterApplication { + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(ConverterApplication::class.java, *args) + } + } +} diff --git a/src/main/kotlin/de/kontextwork/converter/base/FileNameUtils.kt b/src/main/kotlin/de/kontextwork/converter/base/FileNameUtils.kt new file mode 100644 index 0000000..736a380 --- /dev/null +++ b/src/main/kotlin/de/kontextwork/converter/base/FileNameUtils.kt @@ -0,0 +1,12 @@ +package de.kontextwork.converter.base + +import org.springframework.stereotype.Component +import java.nio.file.Paths +import kotlin.io.path.name + +@Component +class FileNameUtils { + fun extractFilenameOnly(originalFileName: String): String { + return Paths.get(originalFileName).name + } +} diff --git a/src/main/kotlin/de/kontextwork/converter/module/convert/ConverterService.kt b/src/main/kotlin/de/kontextwork/converter/module/convert/ConverterService.kt new file mode 100644 index 0000000..9a42b26 --- /dev/null +++ b/src/main/kotlin/de/kontextwork/converter/module/convert/ConverterService.kt @@ -0,0 +1,46 @@ +package de.kontextwork.converter.module.convert + +import de.kontextwork.converter.module.convert.api.UnknownSourceFormatException +import org.apache.commons.io.FilenameUtils +import org.jodconverter.core.DocumentConverter +import org.jodconverter.core.document.DefaultDocumentFormatRegistry +import org.jodconverter.core.document.DocumentFormat +import org.jodconverter.core.office.OfficeException +import org.jodconverter.core.office.OfficeManager +import org.jodconverter.local.LocalConverter +import org.springframework.stereotype.Service +import java.io.ByteArrayOutputStream +import java.io.InputStream + +@Service +class ConverterService( + private val officeManager: OfficeManager +) { + @Throws(UnknownSourceFormatException::class, OfficeException::class) + fun doConvert( + targetFormat: DocumentFormat, + inputFile: InputStream, + inputFileName: String + ): ByteArrayOutputStream { + val outputStream = ByteArrayOutputStream() + val converter: DocumentConverter = LocalConverter.builder().officeManager(officeManager).build() + + val sourceFormat = DefaultDocumentFormatRegistry.getFormatByExtension( + FilenameUtils.getExtension(inputFileName) + ) + ?: throw UnknownSourceFormatException( + String.format( + "Cannot convert file with extension %s since we cannot find the format in our registry", + FilenameUtils.getExtension(inputFileName) + ) + ) + + // Convert... + converter.convert(inputFile) + .`as`(sourceFormat) + .to(outputStream) + .`as`(targetFormat) + .execute() + return outputStream + } +} diff --git a/src/main/kotlin/de/kontextwork/converter/module/convert/api/UnknownSourceFormatException.kt b/src/main/kotlin/de/kontextwork/converter/module/convert/api/UnknownSourceFormatException.kt new file mode 100644 index 0000000..06842a1 --- /dev/null +++ b/src/main/kotlin/de/kontextwork/converter/module/convert/api/UnknownSourceFormatException.kt @@ -0,0 +1,3 @@ +package de.kontextwork.converter.module.convert.api + +class UnknownSourceFormatException(message: String) : Exception(message) diff --git a/src/main/kotlin/de/kontextwork/converter/module/convert/controller/ConversionController.kt b/src/main/kotlin/de/kontextwork/converter/module/convert/controller/ConversionController.kt new file mode 100644 index 0000000..b848f25 --- /dev/null +++ b/src/main/kotlin/de/kontextwork/converter/module/convert/controller/ConversionController.kt @@ -0,0 +1,66 @@ +package de.kontextwork.converter.module.convert.controller + +import de.kontextwork.converter.base.FileNameUtils +import de.kontextwork.converter.module.convert.ConverterService +import de.kontextwork.converter.module.convert.api.UnknownSourceFormatException +import org.apache.commons.io.FilenameUtils +import org.apache.logging.log4j.kotlin.Logging +import org.jodconverter.core.document.DefaultDocumentFormatRegistry +import org.jodconverter.core.office.OfficeException +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile +import java.io.ByteArrayOutputStream +import java.io.IOException + +@RestController +@RequestMapping("/conversion") +class ConversionController( + private val converterService: ConverterService, + private val fileNameUtils: FileNameUtils +) : Logging { + @PostMapping(path = [""]) + @Throws(IOException::class) + fun convert( + @RequestParam(name = "format", defaultValue = "pdf") targetFormatExt: String, + @RequestParam("file") inputMultipartFile: MultipartFile + ): ResponseEntity<*> { + val targetFormat = DefaultDocumentFormatRegistry.getFormatByExtension(targetFormatExt) + if (targetFormat == null) { + logger.error(String.format("Unknown conversion target %s", targetFormatExt)) + return ResponseEntity.status(HttpStatus.PRECONDITION_FAILED).build() + } + + // we preserve the extension only. Anything else could be dangerous + val inputFilename = fileNameUtils.extractFilenameOnly(inputMultipartFile.originalFilename!!) + + val convertedFile: ByteArrayOutputStream = try { + converterService.doConvert(targetFormat, inputMultipartFile.inputStream, inputFilename) + } catch (unknownSourceFormatException: UnknownSourceFormatException) { + logger.error(unknownSourceFormatException.message!!) + return ResponseEntity + .status(HttpStatus.PRECONDITION_FAILED) + .body(unknownSourceFormatException.message) + } catch (officeException: OfficeException) { + logger.error(officeException.message!!) + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .build() + } + // else + + val headers = HttpHeaders() + val targetFilename = "${FilenameUtils.getBaseName(inputFilename)}.${targetFormat.extension}" + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=$targetFilename") + headers.contentType = MediaType.parseMediaType(targetFormat.mediaType) + return ResponseEntity.ok().headers(headers).body(convertedFile.toByteArray()) + } + + +} diff --git a/src/test/java/de/kontextwork/converter/testingUtils/profiles/SetupE2eTest.java b/src/test/java/de/kontextwork/converter/testingUtils/profiles/SetupE2eTest.java deleted file mode 100644 index b5b8d9c..0000000 --- a/src/test/java/de/kontextwork/converter/testingUtils/profiles/SetupE2eTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package de.kontextwork.converter.testingUtils.profiles; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -/** - * use that for full stack tests against real *SQL/mongodb databases with prefilled test DBs - * dwtest1 / dwtest 2 - */ -@SuppressWarnings("unused") -@Tag("e2e") -@ActiveProfiles("e2e") -@ExtendWith(SpringExtension.class) -@ExtendWith(MockitoExtension.class) -@Retention(RetentionPolicy.RUNTIME) -public @interface SetupE2eTest -{} diff --git a/src/test/java/de/kontextwork/converter/testingUtils/profiles/SetupItTest.java b/src/test/java/de/kontextwork/converter/testingUtils/profiles/SetupItTest.java deleted file mode 100644 index 4f989e8..0000000 --- a/src/test/java/de/kontextwork/converter/testingUtils/profiles/SetupItTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package de.kontextwork.converter.testingUtils.profiles; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -/** - * use this for integration tests and most probably combine this with a test-slice - */ -@Tag("it") -@ActiveProfiles("it") -@ExtendWith(SpringExtension.class) -@ExtendWith(MockitoExtension.class) -@Import({}) -@Retention(RetentionPolicy.RUNTIME) -public @interface SetupItTest -{ -} diff --git a/src/test/java/de/kontextwork/converter/testingUtils/profiles/SetupUnitTest.java b/src/test/java/de/kontextwork/converter/testingUtils/profiles/SetupUnitTest.java deleted file mode 100644 index 919dda4..0000000 --- a/src/test/java/de/kontextwork/converter/testingUtils/profiles/SetupUnitTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.kontextwork.converter.testingUtils.profiles; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.context.ActiveProfiles; - -/** - * Pure unit tests, no spring bootstrap - fast any easy - */ -@Tag("unit") -@ActiveProfiles("unit") -@ExtendWith(MockitoExtension.class) -@Retention(RetentionPolicy.RUNTIME) -public @interface SetupUnitTest -{} diff --git a/src/test/java/de/kontextwork/converter/testingUtils/slices/CustomSpringBootTestSlice.java b/src/test/java/de/kontextwork/converter/testingUtils/slices/CustomSpringBootTestSlice.java deleted file mode 100644 index 5fd7fcc..0000000 --- a/src/test/java/de/kontextwork/converter/testingUtils/slices/CustomSpringBootTestSlice.java +++ /dev/null @@ -1,17 +0,0 @@ -package de.kontextwork.converter.testingUtils.slices; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import org.junit.jupiter.api.Tag; -import org.springframework.boot.test.context.SpringBootTest; - -/** - * Use this test slice for a full spring boot test bootstrap, including database, controllers and everything else - */ -@SuppressWarnings("unused") -@SpringBootTest -@Retention(RetentionPolicy.RUNTIME) -public @interface CustomSpringBootTestSlice -{ - -} diff --git a/src/test/java/de/kontextwork/converter/testingUtils/slices/CustomWebMvcWithJpaTestSlice.java b/src/test/java/de/kontextwork/converter/testingUtils/slices/CustomWebMvcWithJpaTestSlice.java deleted file mode 100644 index 57ef05f..0000000 --- a/src/test/java/de/kontextwork/converter/testingUtils/slices/CustomWebMvcWithJpaTestSlice.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.kontextwork.converter.testingUtils.slices; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import org.junit.jupiter.api.Tag; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.test.autoconfigure.core.AutoConfigureCache; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; - -/** - * Use this test slice for a full spring boot test bootstrap, including database, controllers and everything else - */ -@SpringBootTest( - properties = { - "logging.level.org.springframework.web=TRACE" - } -) -@AutoConfigureCache -@AutoConfigureMockMvc -@ImportAutoConfiguration -@Retention(RetentionPolicy.RUNTIME) -@Tag("controller-test") -public @interface CustomWebMvcWithJpaTestSlice -{ - -} diff --git a/src/test/java/de/kontextwork/converter/web/ConversionControllerTest.java b/src/test/java/de/kontextwork/converter/web/ConversionControllerTest.java deleted file mode 100644 index f6015fc..0000000 --- a/src/test/java/de/kontextwork/converter/web/ConversionControllerTest.java +++ /dev/null @@ -1,133 +0,0 @@ -package de.kontextwork.converter.web; - -import de.kontextwork.converter.testingUtils.profiles.SetupItTest; -import de.kontextwork.converter.testingUtils.slices.CustomWebMvcWithJpaTestSlice; -import org.apache.commons.io.FilenameUtils; -import org.apache.tika.Tika; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.Resource; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.test.web.servlet.MockMvc; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@SetupItTest -@CustomWebMvcWithJpaTestSlice -class ConversionControllerTest -{ - @Value("classpath:testfiles/withpictures.docx") - private Resource testDocx; - - @Value("classpath:testfiles/template.dotx") - private Resource testDotx; - - @Value("classpath:testfiles/template.xltx") - private Resource testXltx; - - final private String requestUrl = "/conversion"; - - @Autowired - private MockMvc mockMvc; - - @Test - @DisplayName("Should convert docx to html") - void convertWorks() throws Exception - { - var tika = new Tika(); - Resource testResource = testDocx; - - String mimeType = tika.detect(testResource.getFile()); - var testFile = new MockMultipartFile("file", testResource.getFilename(), mimeType, testResource.getInputStream()); - - var targetFilename = String.format("%s.%s", FilenameUtils.getBaseName(testResource.getFilename()), "html"); - var targetMimeType = tika.detect(targetFilename); // should be html obviously - - mockMvc - .perform( - multipart(requestUrl) - .file(testFile) - .param("format", "html") - ) - .andExpect(status().isOk()) - .andExpect(header().string("Content-Disposition", "attachment; filename=" + targetFilename)) - .andExpect(content().contentType(targetMimeType)); - } - - @Test - @DisplayName("Should convert dotx to html") - void convertDotxWorks() throws Exception - { - var tika = new Tika(); - Resource testResource = testDotx; - - String mimeType = tika.detect(testResource.getFile()); - var testFile = new MockMultipartFile("file", testResource.getFilename(), mimeType, testResource.getInputStream()); - - var targetFilename = String.format("%s.%s", FilenameUtils.getBaseName(testResource.getFilename()), "html"); - var targetMimeType = tika.detect(targetFilename); // should be html obviously - - mockMvc - .perform( - multipart(requestUrl) - .file(testFile) - .param("format", "html") - ) - .andExpect(status().isOk()) - .andExpect(header().string("Content-Disposition", "attachment; filename=" + targetFilename)) - .andExpect(content().contentType(targetMimeType)); - } - - @Test - @DisplayName("Should convert xltx to html") - void convertXltxWorks() throws Exception - { - var tika = new Tika(); - Resource testResource = testXltx; - - String mimeType = tika.detect(testResource.getFile()); - var testFile = new MockMultipartFile("file", testResource.getFilename(), mimeType, testResource.getInputStream()); - - var targetFilename = String.format("%s.%s", FilenameUtils.getBaseName(testResource.getFilename()), "html"); - var targetMimeType = tika.detect(targetFilename); // should be html obviously - - mockMvc - .perform( - multipart(requestUrl) - .file(testFile) - .param("format", "html") - ) - .andExpect(status().isOk()) - .andExpect(header().string("Content-Disposition", "attachment; filename=" + targetFilename)) - .andExpect(content().contentType(targetMimeType)); - } - - @Test - @DisplayName("Should convert docx to html using embedded images") - void convertToHtmlEmbedsImages() throws Exception - { - var tika = new Tika(); - Resource testResource = testDocx; - - String mimeType = tika.detect(testResource.getFile()); - var testFile = new MockMultipartFile("file", testResource.getFilename(), mimeType, testResource.getInputStream()); - - var targetFilename = String.format("%s.%s", FilenameUtils.getBaseName(testResource.getFilename()), "html"); - var targetMimeType = tika.detect(targetFilename); // should be html obviously - - mockMvc - .perform( - multipart(requestUrl) - .file(testFile) - .param("format", "html") - ) - .andExpect(status().isOk()) - .andExpect(header().string("Content-Disposition", "attachment; filename=" + targetFilename)) - .andExpect(content().contentType(targetMimeType)) - .andExpect(content().string(containsString("> = [], + @get:AliasFor(annotation = WebMvcTest::class, attribute = "controllers") val controllers: Array> = [] +) { +}