diff --git a/asciidoctorj-springboot-integration-test/build.gradle b/asciidoctorj-springboot-integration-test/build.gradle new file mode 100644 index 000000000..51a522e05 --- /dev/null +++ b/asciidoctorj-springboot-integration-test/build.gradle @@ -0,0 +1,13 @@ +jar.enabled = false + +dependencies { + testCompileOnly project(':asciidoctorj') + testCompile "junit:junit:$junitVersion" + testCompile "org.assertj:assertj-core:$assertjVersion" + testCompile "commons-io:commons-io:$commonsioVersion" + testCompile "org.awaitility:awaitility:4.0.3" + testCompile "com.squareup.okhttp3:okhttp:4.9.1" + testCompile "com.google.code.gson:gson:2.8.6" +} + +test.dependsOn(':asciidoctorj-springboot-integration-test:springboot-app:assemble') diff --git a/asciidoctorj-springboot-integration-test/springboot-app/build.gradle b/asciidoctorj-springboot-integration-test/springboot-app/build.gradle new file mode 100644 index 000000000..80ae1b586 --- /dev/null +++ b/asciidoctorj-springboot-integration-test/springboot-app/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'org.springframework.boot' version '2.4.2' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'java' +} + +group = 'org.asciidoctor.it' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = '1.8' + +repositories { + mavenCentral() +} + +ext { + asciidoctorjVersion = '2.4.2' +} + +dependencies { + implementation project(':asciidoctorj') + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +bootJar { + requiresUnpack '**/jruby-complete-*.jar' + requiresUnpack '**/asciidoctorj-*.jar' +} + +test { + useJUnitPlatform() +} diff --git a/asciidoctorj-springboot-integration-test/springboot-app/settings.gradle b/asciidoctorj-springboot-integration-test/springboot-app/settings.gradle new file mode 100644 index 000000000..4ec29677c --- /dev/null +++ b/asciidoctorj-springboot-integration-test/springboot-app/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'springboot-app' diff --git a/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/AsciidoctorService.java b/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/AsciidoctorService.java new file mode 100644 index 000000000..1aab49cdb --- /dev/null +++ b/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/AsciidoctorService.java @@ -0,0 +1,25 @@ +package org.asciidoctor.it.springboot; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.OptionsBuilder; +import org.asciidoctor.SafeMode; +import org.springframework.stereotype.Component; + +@Component +public class AsciidoctorService { + + private final Asciidoctor asciidoctor = Asciidoctor.Factory.create(); + + + public String convert(String source) { + return asciidoctor.convert(source, defaultOptions()); + } + + private OptionsBuilder defaultOptions() { + return OptionsBuilder.options() + .backend("html5") + .safe(SafeMode.SAFE) + .headerFooter(true) + .toFile(false); + } +} diff --git a/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/ConvertedResource.java b/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/ConvertedResource.java new file mode 100644 index 000000000..1c7831848 --- /dev/null +++ b/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/ConvertedResource.java @@ -0,0 +1,23 @@ +package org.asciidoctor.it.springboot; + +import java.beans.ConstructorProperties; + +public class ConvertedResource { + + private final String content; + private final String contentType; + + @ConstructorProperties({"content", "contentType"}) + public ConvertedResource(String content, String contentType) { + this.content = content; + this.contentType = contentType; + } + + public String getContent() { + return content; + } + + public String getContentType() { + return contentType; + } +} diff --git a/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/ConverterController.java b/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/ConverterController.java new file mode 100644 index 000000000..c05e6bb03 --- /dev/null +++ b/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/ConverterController.java @@ -0,0 +1,33 @@ +package org.asciidoctor.it.springboot; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + + +/** + * Simple Spring MVC Rest Controller to convert AsciiDoc sources to HTML. + */ +@RestController +public class ConverterController { + + private final AsciidoctorService asciidoctorService; + + public ConverterController(AsciidoctorService asciidoctorService) { + this.asciidoctorService = asciidoctorService; + } + + + @PostMapping("/asciidoc") + public ConvertedResource convert(@RequestBody SourceContent content) { + byte[] decodedContent = Base64.getDecoder().decode(content.getData()); + String converted = asciidoctorService.convert(new String(decodedContent)); + + String encodedConvertedContent = Base64.getEncoder().encodeToString(converted.getBytes(StandardCharsets.UTF_8)); + return new ConvertedResource(encodedConvertedContent, MediaType.TEXT_HTML_VALUE); + } +} diff --git a/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/SourceContent.java b/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/SourceContent.java new file mode 100644 index 000000000..ca1b94da6 --- /dev/null +++ b/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/SourceContent.java @@ -0,0 +1,18 @@ +package org.asciidoctor.it.springboot; + +public class SourceContent { + + /** + * Base64 encoded + */ + private String data; + + + public void setData(String data) { + this.data = data; + } + + public String getData() { + return data; + } +} diff --git a/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/SpringbootAppApplication.java b/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/SpringbootAppApplication.java new file mode 100644 index 000000000..f07922989 --- /dev/null +++ b/asciidoctorj-springboot-integration-test/springboot-app/src/main/java/org/asciidoctor/it/springboot/SpringbootAppApplication.java @@ -0,0 +1,18 @@ +package org.asciidoctor.it.springboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * This is a test application for testing purposes. + * It does not in any represent the recommended way to implement an + * Asciidoctorj conversion service with SpringBoot. + */ +@SpringBootApplication +public class SpringbootAppApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringbootAppApplication.class, args); + } + +} diff --git a/asciidoctorj-springboot-integration-test/springboot-app/src/main/resources/application.properties b/asciidoctorj-springboot-integration-test/springboot-app/src/main/resources/application.properties new file mode 100644 index 000000000..6a7ab879e --- /dev/null +++ b/asciidoctorj-springboot-integration-test/springboot-app/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.main.banner-mode=off +management.endpoint.health.probes.enabled=true +info.app.java.source=${java.version} +info.app.java.target=${java.version} diff --git a/asciidoctorj-springboot-integration-test/springboot-app/src/test/java/org/asciidoctor/it/springboot/SpringbootAppApplicationTests.java b/asciidoctorj-springboot-integration-test/springboot-app/src/test/java/org/asciidoctor/it/springboot/SpringbootAppApplicationTests.java new file mode 100644 index 000000000..cf8f12d7c --- /dev/null +++ b/asciidoctorj-springboot-integration-test/springboot-app/src/test/java/org/asciidoctor/it/springboot/SpringbootAppApplicationTests.java @@ -0,0 +1,56 @@ +package org.asciidoctor.it.springboot; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class SpringbootAppApplicationTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void should_convert_document() { + + final SourceContent request = new SourceContent(); + String asciidocDocument = "= Title\n" + + "\n" + + "== First chapter\n" + + "first chapter\n" + + "\n" + + "== Second chapter\n" + + "second chapter"; + request.setData(base64Encode(asciidocDocument)); + + ResponseEntity responseEntity = restTemplate.postForEntity("/asciidoc", request, ConvertedResource.class); + + assertThat(responseEntity.getStatusCodeValue()) + .isEqualTo(HttpStatus.OK.value()); + final ConvertedResource response = responseEntity.getBody(); + assertThat(response.getContentType()) + .isEqualTo(MediaType.TEXT_HTML_VALUE); + + assertThat(base64Decode(response)) + .contains("

Title

") + .contains("

first chapter

") + .contains("

Second chapter

"); + } + + private String base64Encode(String asciidocDocument) { + return new String(Base64.getEncoder().encode(asciidocDocument.getBytes(StandardCharsets.UTF_8))); + } + + private String base64Decode(ConvertedResource response) { + return new String(Base64.getDecoder().decode(response.getContent())); + } +} diff --git a/asciidoctorj-springboot-integration-test/src/test/java/org/asciidoctor/it/springboot/ProcessHelper.java b/asciidoctorj-springboot-integration-test/src/test/java/org/asciidoctor/it/springboot/ProcessHelper.java new file mode 100644 index 000000000..d951b3917 --- /dev/null +++ b/asciidoctorj-springboot-integration-test/src/test/java/org/asciidoctor/it/springboot/ProcessHelper.java @@ -0,0 +1,52 @@ +package org.asciidoctor.it.springboot; + +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class ProcessHelper { + + static ProcessOutput run(Map env, String... command) throws IOException, InterruptedException { + ProcessBuilder processBuilder = new ProcessBuilder(command) + .directory(new File("springboot-app")); + + processBuilder.environment().clear(); + String javaHome = System.getProperty("java.home"); + processBuilder.environment().put("JAVA_HOME", javaHome); + processBuilder.environment().put("PATH", javaHome + "/bin"); + processBuilder.environment().putAll(env); + + Process process = processBuilder.start(); + + final InputStream is = process.getInputStream(); + final InputStream es = process.getErrorStream(); + boolean status = process.waitFor(10l, TimeUnit.SECONDS); + if (!status) { + byte[] buffer = new byte[1800]; + IOUtils.read(is, buffer); + return new ProcessOutput(process, new String(buffer), new String()); + } + + return new ProcessOutput(IOUtils.toString(es), IOUtils.toString(is)); + } + + + static class ProcessOutput { + final Process process; + final String out, err; + + public ProcessOutput(String sto, String err) { + this(null, sto, err); + } + + public ProcessOutput(Process process, String sto, String err) { + this.process = process; + this.out = sto; + this.err = err; + } + } +} diff --git a/asciidoctorj-springboot-integration-test/src/test/java/org/asciidoctor/it/springboot/SpringBootTest.java b/asciidoctorj-springboot-integration-test/src/test/java/org/asciidoctor/it/springboot/SpringBootTest.java new file mode 100644 index 000000000..87e2244b2 --- /dev/null +++ b/asciidoctorj-springboot-integration-test/src/test/java/org/asciidoctor/it/springboot/SpringBootTest.java @@ -0,0 +1,100 @@ +package org.asciidoctor.it.springboot; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import kotlin.collections.ArrayDeque; +import okhttp3.*; +import org.awaitility.Awaitility; +import org.junit.After; +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.asciidoctor.it.springboot.ProcessHelper.run; +import static org.assertj.core.api.Assertions.assertThat; + +public class SpringBootTest { + + final List testProcesses = new ArrayDeque<>(); + + + @Test + public void should_start_SpringBoot_service_with_requiresUnpack() throws IOException, InterruptedException { + + int serverPort = 8000 + ThreadLocalRandom.current().nextInt(1000); + startSpringBootApp(serverPort); + + String asciidocDocument = "= Title\n" + + "\n" + + "== First chapter\n" + + "first chapter\n" + + "\n" + + "== Second chapter\n" + + "second chapter"; + Map jsonResponse = convertDocument(asciidocDocument, serverPort); + + assertThat(jsonResponse.get("contentType")) + .isEqualTo("text/html"); + assertThat(base64Decode(jsonResponse.get("content"))) + .contains("

Title

") + .contains("

first chapter

") + .contains("

Second chapter

"); + } + + @After + public void cleanup() { + testProcesses.forEach(Process::destroy); + } + + private Map convertDocument(String asciidocDocument, int serverPort) throws IOException { + String body = "{\"data\":\"" + base64Encode(asciidocDocument) + "\"}"; + + Request request = new Request.Builder() + .url("http://localhost:" + serverPort + "/asciidoc") + .post(RequestBody.create(body, MediaType.parse("application/json"))) + .build(); + Response response = new OkHttpClient() + .newCall(request) + .execute(); + + return new Gson() + .fromJson(response.body().string(), new TypeToken>() {}.getType()); + } + + private String base64Encode(String asciidocDocument) { + return new String(Base64.getEncoder().encode(asciidocDocument.getBytes(StandardCharsets.UTF_8))); + } + + private String base64Decode(String value) { + return new String(Base64.getDecoder().decode(value)); + } + + private void startSpringBootApp(int serverPort) throws IOException, InterruptedException { + + ProcessHelper.ProcessOutput run = run(singletonMap("SERVER_PORT", String.valueOf(serverPort)), + "java", "-jar", "build/libs/springboot-app-0.0.1-SNAPSHOT.jar"); + + testProcesses.add(run.process); + + Awaitility.await() + .atMost(30, TimeUnit.SECONDS) + .pollInterval(3, TimeUnit.SECONDS) + .until(() -> { + Request request = new Request.Builder() + .url("http://localhost:" + serverPort + "/actuator/health/readiness") + .build(); + Response response = new OkHttpClient() + .newCall(request) + .execute(); + return response.isSuccessful() && response.body().string().contains("UP"); + }); + } +} diff --git a/build.gradle b/build.gradle index d4acb38fe..9c6349ec7 100644 --- a/build.gradle +++ b/build.gradle @@ -66,6 +66,7 @@ ext { jrubyVersion = '9.2.14.0' jsoupVersion = '1.12.1' junitVersion = '4.13.1' + assertjVersion = '3.19.0' nettyVersion = '4.1.58.Final' saxonVersion = '9.9.0-2' xmlMatchersVersion = '1.0-RC1' @@ -135,7 +136,7 @@ subprojects { configFile = rootProject.file('config/codenarc/codenarc.groovy') } - if (it.name != 'asciidoctorj-wildfly-integration-test') { + if (!isIntegrationTestProject(it)) { dependencies { testCompile "junit:junit:$junitVersion" @@ -178,10 +179,16 @@ subprojects { } -configure(subprojects.findAll { !it.isDistribution() && it.name != 'asciidoctorj-api' && it.name != 'asciidoctorj-documentation' && it.name != 'asciidoctorj-test-support' && it.name != 'asciidoctorj-arquillian-extension' && it.name != 'asciidoctorj-wildfly-integration-test' }) { +configure(subprojects.findAll { !it.isDistribution() && it.name != 'asciidoctorj-api' && it.name != 'asciidoctorj-documentation' && it.name != 'asciidoctorj-test-support' && it.name != 'asciidoctorj-arquillian-extension' && !isIntegrationTestProject(it) }) { apply from: rootProject.file('gradle/versioncheck.gradle') } +boolean isIntegrationTestProject(def project) { + project.name in ['asciidoctorj-wildfly-integration-test', + 'asciidoctorj-springboot-integration-test', + 'springboot-app'] +} + // apply JRuby and sources/javadocs packaging stuff for all subprojects except the distribution configure(subprojects.findAll { !it.isDistribution() }) { diff --git a/gradle/versioncheck.gradle b/gradle/versioncheck.gradle index ccf26fd41..167b3e747 100644 --- a/gradle/versioncheck.gradle +++ b/gradle/versioncheck.gradle @@ -91,4 +91,4 @@ private String latestGemVersionAsJavaVersion(String gem_latest_version) { checkVersion.onlyIf { !gradle.startParameter.isOffline() } checkVersion.onlyIf { !project.hasProperty("skip.checkVersion") } -check.dependsOn checkVersion \ No newline at end of file +check.dependsOn checkVersion diff --git a/settings.gradle b/settings.gradle index 5d7afbb4d..69084fd12 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,6 +7,8 @@ include \ 'asciidoctorj-distribution', 'asciidoctorj-documentation', 'asciidoctorj-wildfly-integration-test', + 'asciidoctorj-springboot-integration-test', + 'asciidoctorj-springboot-integration-test:springboot-app', 'asciidoctorj-test-support' project(':asciidoctorj').projectDir = file('asciidoctorj-core')