diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 811936b7..58dfc9c2 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -19,6 +19,7 @@ Improvements:: * Make `auto-refresh` (and `http` by inheritance) only convert modified and created sources (#474) * Make `auto-refresh` only copy modified and created resources + taking into consideration options (#478) * Make `auto-refresh` ignore docInfo files to avoid copying them into output (#480) + * Add official support for `http` mojo with life preview and refresh of html output (#483) Bug Fixes:: diff --git a/README.adoc b/README.adoc index b4180e95..e8a9af8d 100644 --- a/README.adoc +++ b/README.adoc @@ -415,7 +415,8 @@ An example of this setup is below: <.> Any configuration outside the executions section is inherited by each execution. This allows an easier way to share common configuration options. -=== Automatic conversion of documents on change (refresh) +[[auto-refresh-goal]] +=== Automatic conversion of documents on change (`auto-refresh`) Using the `auto-refresh` goal it is possible to convert documents when one of them is modified, or a new one is added. No need for a full rebuild or typing any command. @@ -452,6 +453,7 @@ This is specially useful in combination with a refresh browser extension, allowi Once started, this will keep the maven process running until you enter `exit` or `quit` command in the console. Or it is manually killed with _ctrl+c_. +[[auto-refresh-goal-config-note]] [NOTE] ==== It is possible to run `auto-refresh` on your current project without changes provided the configuration is at top plugin level. @@ -503,6 +505,70 @@ Defaults to `2000` If `failIf` is set and errors are introduced after start, these will be reported but the plugin will continue running. If errors are found during initialization, plugin won't start. +[[http-goal]] +=== HTML life preview (`http`) + +The `http` goal allows starting an embedded http server to access content from the generated output directory, while the plugin updates it. + +Modified sources will be updated similarly to how <> works. +And at the same time, HTML contents will be automatically refreshed on the web browser without need for manual steps. +Just open the file through the provided url that will appear in the console and write. + +Note than the file extension is not necessary for html files. +Bu default, the document _manual.html_ placed in the root path will be accessible as _pass:c[http://localhost:2000/manual]_. + +[NOTE] +==== +While the `http` goal can be used to serve any kind of content (e.g. PDF). +The possibilities have not been explored and are not officially supported, but feedback is welcome if you want to share your experience and ideas. +==== + +[source,xml] +.Http configuration extract +---- + + ... + + + output-html + generate-resources + + http + + + 8080 + + + false + coderay + + + + + +---- +<1> The asciidoctor-maven-plugin does not run in any phase by default, so one must be specified. +<2> The Asciidoctor Maven plugin http goal. +<3> Asciidoctor options. +Here we change the port to 8080. + +This feature shares the following features (and limitations) with <> goal. + +* Once started, this will keep the maven process running until explicitly stopped (`exit`, `quit` commands or _ctrl+c_). +* Deleted or moved files will remain the output directory until clean. +* To take full advantage of configuration options, it must be explicitly configured in _pom.xml_ (see <>). +* `failIf` configurations will make the goal fail to start, but won't stop the server once started. + +==== Configuration + +The mojo accepts the same configurations as `process-asciidoc`, `refresh` mojos and adds: + +port:: server port. +Defaults to `2000`. + +home:: default resource to open when no url is indicated, that is when browsing to http://localhost:2000 by default. +Defaults to `index`. + == Maven Site Integration === Setup diff --git a/src/main/java/org/asciidoctor/maven/AsciidoctorHttpMojo.java b/src/main/java/org/asciidoctor/maven/AsciidoctorHttpMojo.java index 80b3f2b1..cb95b5fd 100644 --- a/src/main/java/org/asciidoctor/maven/AsciidoctorHttpMojo.java +++ b/src/main/java/org/asciidoctor/maven/AsciidoctorHttpMojo.java @@ -1,18 +1,10 @@ package org.asciidoctor.maven; -import org.apache.commons.io.IOUtils; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; -import org.asciidoctor.Asciidoctor; import org.asciidoctor.maven.http.AsciidoctorHttpServer; -import org.asciidoctor.maven.io.IO; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.Map; @Mojo(name = "http") public class AsciidoctorHttpMojo extends AsciidoctorRefreshMojo { @@ -25,60 +17,18 @@ public class AsciidoctorHttpMojo extends AsciidoctorRefreshMojo { @Parameter(property = PREFIX + "home", defaultValue = "index") protected String home; - @Parameter(property = PREFIX + "reload-interval", defaultValue = "0") - protected int autoReloadInterval; - @Override public void execute() throws MojoExecutionException, MojoFailureException { + final AsciidoctorHttpServer server = new AsciidoctorHttpServer(getLog(), port, outputDirectory, home); + + startPolling(); server.start(); doWork(); doWait(); server.stop(); - } - - @Override - protected void convertFile(final Asciidoctor asciidoctorInstance, final Map options, final File f) { - asciidoctorInstance.convertFile(f, options); - logConvertedFile(f); - - if (autoReloadInterval > 0 && backend.toLowerCase().startsWith("html")) { - final String filename = f.getName(); - final File out = new File(outputDirectory, filename.substring(0, filename.lastIndexOf(".")) + ".html"); - if (out.exists()) { - - String content = null; - - { // read - FileInputStream fis = null; - try { - fis = new FileInputStream(out); // java asciidoctor render() doesn't work ATM so read the converted file instead of doing it in memory - content = IO.slurp(fis); - } catch (final Exception e) { - getLog().error(e); - } finally { - IOUtils.closeQuietly(fis); - } - } - - if (content != null) { // convert + write - FileOutputStream fos = null; - try { - fos = new FileOutputStream(out); - fos.write(addRefreshing(content).getBytes()); - } catch (final Exception e) { - getLog().error(e); - } finally { - IOUtils.closeQuietly(fos); - } - } - } - } - } - - private String addRefreshing(final String html) { - return html.replace("", "\n"); + stopMonitors(); } public String getHome() { diff --git a/src/main/java/org/asciidoctor/maven/AsciidoctorRefreshMojo.java b/src/main/java/org/asciidoctor/maven/AsciidoctorRefreshMojo.java index acb0eb33..6b4bbbf4 100644 --- a/src/main/java/org/asciidoctor/maven/AsciidoctorRefreshMojo.java +++ b/src/main/java/org/asciidoctor/maven/AsciidoctorRefreshMojo.java @@ -76,7 +76,7 @@ private void showWaitMessage() { getLog().info("Type [exit|quit] to exit and [refresh] to force a manual re-conversion."); } - private void stopMonitors() throws MojoExecutionException { + protected void stopMonitors() throws MojoExecutionException { if (monitors != null) { for (final FileAlterationMonitor monitor : monitors) { try { @@ -88,7 +88,7 @@ private void stopMonitors() throws MojoExecutionException { } } - private void startPolling() throws MojoExecutionException { + protected void startPolling() throws MojoExecutionException { // TODO avoid duplication with AsciidoctorMojo final Optional sourceDirectoryCandidate = findSourceDirectory(sourceDirectory, project.getBasedir()); diff --git a/src/main/java/org/asciidoctor/maven/http/AsciidoctorHandler.java b/src/main/java/org/asciidoctor/maven/http/AsciidoctorHandler.java index d68a8ac8..73059f77 100644 --- a/src/main/java/org/asciidoctor/maven/http/AsciidoctorHandler.java +++ b/src/main/java/org/asciidoctor/maven/http/AsciidoctorHandler.java @@ -1,24 +1,22 @@ package org.asciidoctor.maven.http; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; - import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.*; import io.netty.util.CharsetUtil; +import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.nio.charset.StandardCharsets; + public class AsciidoctorHandler extends SimpleChannelInboundHandler { + private static final String HTML_MEDIA_TYPE = "text/html"; public static final String HTML_EXTENSION = ".html"; @@ -37,40 +35,57 @@ public AsciidoctorHandler(final File workDir, final String defaultPage) { @Override public void channelRead0(final ChannelHandlerContext ctx, final FullHttpRequest msg) throws Exception { - if (msg.getMethod() != HttpMethod.GET) { - final DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, - HttpResponseStatus.METHOD_NOT_ALLOWED, - Unpooled.copiedBuffer("Only GET method allowed", CharsetUtil.UTF_8)); + + if (msg.getMethod() != HttpMethod.GET && msg.getMethod() != HttpMethod.HEAD) { + send(ctx, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.METHOD_NOT_ALLOWED)); + return; + } + + final File file = deduceFile(msg.getUri()); + + if (!file.exists()) { + final ByteBuf body = Unpooled.copiedBuffer("File not found: " + file.getPath() + "", CharsetUtil.UTF_8); + final DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND, body); response.headers().set(HttpHeaders.Names.CONTENT_TYPE, HTML_MEDIA_TYPE); send(ctx, response); return; } - final File file = deduceFile(msg.getUri()); + // HEAD means we already loaded the page, so we know is HTML + if (msg.getMethod() == HttpMethod.HEAD) { + final DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.RESET_CONTENT); + + final HttpHeaders headers = response.headers(); + // Test if retuning any size works + headers.set(HttpHeaders.Names.CONTENT_LENGTH, file.length()); + headers.set(HttpHeaders.Names.EXPIRES, 0); + headers.set(HttpHeaders.Names.CONTENT_TYPE, HTML_MEDIA_TYPE); + send(ctx, response); + return; + } - final HttpResponseStatus status; final ByteBuf body; - final String mediaType; - if (file.exists()) { + + if (file.getName().endsWith("html")) { + final String content = FileUtils.readFileToString(file, StandardCharsets.UTF_8); + body = Unpooled.copiedBuffer(addRefreshing(content), CharsetUtil.UTF_8); + } else { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final FileInputStream fileInputStream = new FileInputStream(file); IOUtils.copy(fileInputStream, baos); - body = Unpooled.copiedBuffer(baos.toByteArray()); + body = Unpooled.copiedBuffer(FileUtils.readFileToByteArray(file)); IOUtils.closeQuietly(fileInputStream); - - mediaType = mediaType(file.getName()); - status = HttpResponseStatus.OK; - } else { - body = Unpooled.copiedBuffer("File not found: " + file.getPath() + "", CharsetUtil.UTF_8); - status = HttpResponseStatus.NOT_FOUND; - mediaType = HTML_MEDIA_TYPE; } - final DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, body); - response.headers().set(HttpHeaders.Names.CONTENT_TYPE, mediaType); + final DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, body); + response.headers().set(HttpHeaders.Names.CONTENT_TYPE, mediaType(file.getName())); send(ctx, response); } + private String addRefreshing(final String html) { + return html.replace("", ""); + } + private void send(final ChannelHandlerContext ctx, final DefaultFullHttpResponse response) { ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } @@ -80,11 +95,7 @@ private File deduceFile(final String path) { return new File(directory, defaultPage); } - if (!path.contains(".")) { - return new File(directory, addDefaultExtension(path)); - } - - return new File(directory, path); + return new File(directory, path.contains(".") ? path : addDefaultExtension(path)); } private static String addDefaultExtension(String path) { @@ -92,6 +103,9 @@ private static String addDefaultExtension(String path) { } private static String mediaType(final String name) { + if (name.endsWith(".html")) { + return HTML_MEDIA_TYPE; + } if (name.endsWith(".js")) { return "text/javascript"; } @@ -107,6 +121,6 @@ private static String mediaType(final String name) { if (name.endsWith(".jpeg") || name.endsWith(".jpg")) { return "image/jpeg"; } - return HTML_MEDIA_TYPE; + return "application/octet-stream"; } } diff --git a/src/main/java/org/asciidoctor/maven/http/AsciidoctorHttpServer.java b/src/main/java/org/asciidoctor/maven/http/AsciidoctorHttpServer.java index fe4601b5..c638f856 100644 --- a/src/main/java/org/asciidoctor/maven/http/AsciidoctorHttpServer.java +++ b/src/main/java/org/asciidoctor/maven/http/AsciidoctorHttpServer.java @@ -41,7 +41,7 @@ public AsciidoctorHttpServer(final Log logger, final int port, final File output this.defaultPage = defaultPage; } - public void start() { + public AsciidoctorHttpServer start() { final AtomicInteger threadId = new AtomicInteger(1); workerGroup = new NioEventLoopGroup(THREAD_NUMBER, new ThreadFactory() { @Override @@ -89,6 +89,7 @@ public void operationComplete(final ChannelFuture future) throws Exception { } catch (final InterruptedException e) { logger.error(e.getMessage(), e); } + return this; } public void stop() { diff --git a/src/test/groovy/org/asciidoctor/maven/test/AsciidoctorHttpMojoTest.groovy b/src/test/groovy/org/asciidoctor/maven/test/AsciidoctorHttpMojoTest.groovy index 37352835..ca3f4a3d 100644 --- a/src/test/groovy/org/asciidoctor/maven/test/AsciidoctorHttpMojoTest.groovy +++ b/src/test/groovy/org/asciidoctor/maven/test/AsciidoctorHttpMojoTest.groovy @@ -8,6 +8,7 @@ import org.asciidoctor.maven.io.TestFilesHelper import org.asciidoctor.maven.test.plexus.MockPlexusContainer import spock.lang.Specification +import java.nio.file.Files import java.util.concurrent.CountDownLatch class AsciidoctorHttpMojoTest extends Specification { @@ -76,7 +77,7 @@ class AsciidoctorHttpMojoTest extends Specification { awaitTermination(mojoThread) } - def "default page"() { + def "should return default page"() { setup: def srcDir = new File('target/test-classes/src/asciidoctor-http-default') def outputDir = TestFilesHelper.newOutputTestDirectory('http-mojo') @@ -135,6 +136,162 @@ class AsciidoctorHttpMojoTest extends Specification { awaitTermination(mojoThread) } + def "should return 404 when file does not exist"() { + setup: + def emptySrcDir = new File('some_path') + def outputDir = TestFilesHelper.newOutputTestDirectory('http-mojo') + + def inputLatch = new CountDownLatch(1) + + def originalOut = System.out + def originalIn = System.in + + def newOut = new DoubleOutputStream(originalOut) + def newIn = new PrefilledInputStream('exit\r\nexit\r\nexit\r\n'.bytes, inputLatch) + + def httpPort = availablePort + + System.setOut(new PrintStream(newOut)) + System.setIn(newIn) + + def mojo = new AsciidoctorHttpMojo() + mojo.backend = 'html5' + mojo.port = httpPort + mojo.sourceDirectory = emptySrcDir + mojo.outputDirectory = outputDir + mojo.headerFooter = true + mojo.home = 'content' + def mojoThread = new Thread(new Runnable() { + @Override + void run() { + mojo.execute() + } + }) + mojoThread.start() + + while (!new String(newOut.toByteArray()).contains('Type ')) { + Thread.sleep(200) + } + + when: + HttpURLConnection connection = new URL("http://localhost:${httpPort}/").openConnection() + def status = connection.getResponseCode() + + then: + status == 404 + + cleanup: + System.setOut(originalOut) + inputLatch.countDown() + System.setIn(originalIn) + awaitTermination(mojoThread) + } + + def "should return 405 when method is not POST"() { + setup: + def emptySrcDir = new File('some_path') + def outputDir = TestFilesHelper.newOutputTestDirectory('http-mojo') + + def inputLatch = new CountDownLatch(1) + + def originalOut = System.out + def originalIn = System.in + + def newOut = new DoubleOutputStream(originalOut) + def newIn = new PrefilledInputStream('exit\r\nexit\r\nexit\r\n'.bytes, inputLatch) + + def httpPort = availablePort + + System.setOut(new PrintStream(newOut)) + System.setIn(newIn) + + def mojo = new AsciidoctorHttpMojo() + mojo.backend = 'html5' + mojo.port = httpPort + mojo.sourceDirectory = emptySrcDir + mojo.outputDirectory = outputDir + mojo.headerFooter = true + mojo.home = 'content' + def mojoThread = new Thread(new Runnable() { + @Override + void run() { + mojo.execute() + } + }) + mojoThread.start() + + while (!new String(newOut.toByteArray()).contains('Type ')) { + Thread.sleep(200) + } + + when: + HttpURLConnection connection = new URL("http://localhost:${httpPort}/").openConnection() + connection.setRequestMethod("POST") + def status = connection.getResponseCode() + + then: + status == 405 + + cleanup: + System.setOut(originalOut) + inputLatch.countDown() + System.setIn(originalIn) + awaitTermination(mojoThread) + } + + def "should return 205 when method is HEAD and resource exists"() { + setup: + def emptySrcDir = new File('some_path') + def outputDir = TestFilesHelper.newOutputTestDirectory('http-mojo') + TestFilesHelper.createFileWithContent(outputDir,'index.html') + + def inputLatch = new CountDownLatch(1) + + def originalOut = System.out + def originalIn = System.in + + def newOut = new DoubleOutputStream(originalOut) + def newIn = new PrefilledInputStream('exit\r\nexit\r\nexit\r\n'.bytes, inputLatch) + + def httpPort = availablePort + + System.setOut(new PrintStream(newOut)) + System.setIn(newIn) + + def mojo = new AsciidoctorHttpMojo() + mojo.backend = 'html5' + mojo.port = httpPort + mojo.sourceDirectory = emptySrcDir + mojo.outputDirectory = outputDir + mojo.headerFooter = true + mojo.home = 'index' + def mojoThread = new Thread(new Runnable() { + @Override + void run() { + mojo.execute() + } + }) + mojoThread.start() + + while (!new String(newOut.toByteArray()).contains('Type ')) { + Thread.sleep(200) + } + + when: + HttpURLConnection connection = new URL("http://localhost:${httpPort}/").openConnection() + connection.setRequestMethod("HEAD") + def status = connection.getResponseCode() + + then: + status == 205 + + cleanup: + System.setOut(originalOut) + inputLatch.countDown() + System.setIn(originalIn) + awaitTermination(mojoThread) + } + private int getAvailablePort() { ServerSocket socket = new ServerSocket(0) int port = socket.getLocalPort() diff --git a/src/test/java/org/asciidoctor/maven/http/AsciidoctorHttpServerTest.java b/src/test/java/org/asciidoctor/maven/http/AsciidoctorHttpServerTest.java new file mode 100644 index 00000000..110d94e0 --- /dev/null +++ b/src/test/java/org/asciidoctor/maven/http/AsciidoctorHttpServerTest.java @@ -0,0 +1,293 @@ +package org.asciidoctor.maven.http; + +import lombok.SneakyThrows; +import lombok.Value; +import org.apache.commons.io.IOUtils; +import org.apache.maven.plugin.logging.Log; +import org.junit.Test; +import org.mockito.Mockito; + +import java.io.File; +import java.io.InputStream; +import java.net.ConnectException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Random; +import java.util.UUID; + +import static io.netty.handler.codec.http.HttpResponseStatus.*; +import static org.asciidoctor.maven.io.TestFilesHelper.createFileWithContent; +import static org.asciidoctor.maven.io.TestFilesHelper.newOutputTestDirectory; +import static org.assertj.core.api.Assertions.assertThat; + +public class AsciidoctorHttpServerTest { + + private static final Random RANDOM = new Random(); + + + @Test + public void should_start_and_stop_server() { + // given + int port = randomPort(); + final File outputDir = newOutputTestDirectory(); + final String defaultUrl = "http://localhost:" + port + "/index"; + + // when + final AsciidoctorHttpServer server = + new AsciidoctorHttpServer(Mockito.mock(Log.class), port, outputDir, "index") + .start(); + + final HttpResponse responseWhileStarted = doHttpGet(defaultUrl); + server.stop(); + final HttpResponse responseOnceStopped = doHttpGet(defaultUrl); + + // then + assertThat(responseWhileStarted) + .isEqualTo(HttpResponse.notFound()); + assertThat(responseOnceStopped.getError()) + .isInstanceOf(ConnectException.class) + .hasMessageStartingWith("Connection refuse"); + } + + @Test + public void should_return_404_when_resource_does_not_exist() { + // given + int port = randomPort(); + final File outputDir = newOutputTestDirectory(); + final String resource = "http://localhost:" + port + "/" + UUID.randomUUID(); + + // when + final AsciidoctorHttpServer server = + new AsciidoctorHttpServer(Mockito.mock(Log.class), port, outputDir, "index") + .start(); + + final HttpResponse response = doHttpGet(resource); + + // then + assertThat(response) + .isEqualTo(HttpResponse.notFound()); + + // cleanup + server.stop(); + } + + @Test + public void should_return_405_when_method_is_not_GET_or_HEAD() { + // given + int port = randomPort(); + final File outputDir = newOutputTestDirectory(); + final String resource = "http://localhost:" + port + "/" + UUID.randomUUID(); + + // when + final AsciidoctorHttpServer server = + new AsciidoctorHttpServer(Mockito.mock(Log.class), port, outputDir, "index") + .start(); + + final HttpResponse response = doHttpPost(resource); + + // then + assertThat(response.getStatus()) + .isEqualTo(METHOD_NOT_ALLOWED.code()); + assertThat(response.getMessage()) + .isEqualTo(METHOD_NOT_ALLOWED.reasonPhrase()); + + // cleanup + server.stop(); + } + + @Test + public void should_return_205_when_method_is_HEAD_and_resource_exists() { + // given + int port = randomPort(); + final File outputDir = newOutputTestDirectory(); + createFileWithContent(outputDir, "index.html"); + final String existingResource = "http://localhost:" + port + "/index"; + + // when + final AsciidoctorHttpServer server = + new AsciidoctorHttpServer(Mockito.mock(Log.class), port, outputDir, "index") + .start(); + + final HttpResponse response = doHttpHead(existingResource); + + // then + assertThat(response.getStatus()) + .isEqualTo(RESET_CONTENT.code()); + assertThat(response.getMessage()) + .isEqualTo(RESET_CONTENT.reasonPhrase()); + + // cleanup + server.stop(); + } + + @Test + public void should_return_404_when_method_is_HEAD_does_not_exists() { + // given + int port = randomPort(); + final File outputDir = newOutputTestDirectory(); + final String existingResource = "http://localhost:" + port + "/" + UUID.randomUUID(); + + // when + final AsciidoctorHttpServer server = + new AsciidoctorHttpServer(Mockito.mock(Log.class), port, outputDir, "index") + .start(); + + final HttpResponse response = doHttpHead(existingResource); + + // then + assertThat(response.getStatus()) + .isEqualTo(NOT_FOUND.code()); + assertThat(response.getMessage()) + .isEqualTo(NOT_FOUND.reasonPhrase()); + + // cleanup + server.stop(); + } + + @Test + public void should_return_modified_html_content_without_modifying_original() { + // given + int port = randomPort(); + final File outputDir = newOutputTestDirectory(); + final String testContent = "Test HTML"; + createFileWithContent(outputDir, "index.html", testContent); + final String existingResource = "http://localhost:" + port + "/index"; + + // when + final AsciidoctorHttpServer server = + new AsciidoctorHttpServer(Mockito.mock(Log.class), port, outputDir, "index") + .start(); + + final HttpResponse response = doHttpGet(existingResource); + + // then + assertThat(response.getStatus()) + .isEqualTo(OK.code()); + assertThat(response.getMessage()) + .isEqualTo(OK.reasonPhrase()); + assertThat(response.getContent()) + .isEqualTo("Test HTML"); + + // cleanup + server.stop(); + } + + @Test + public void should_return_non_html_content_without_modifications() { + // given + int port = randomPort(); + final File outputDir = newOutputTestDirectory(); + final String testContent = "almost-a-css {}"; + createFileWithContent(outputDir, "styles.css", testContent); + final String existingResource = "http://localhost:" + port + "/styles.css"; + + // when + final AsciidoctorHttpServer server = + new AsciidoctorHttpServer(Mockito.mock(Log.class), port, outputDir, "index") + .start(); + + final HttpResponse response = doHttpGet(existingResource); + + // then + assertThat(response.getStatus()) + .isEqualTo(OK.code()); + assertThat(response.getMessage()) + .isEqualTo(OK.reasonPhrase()); + assertThat(response.getContent()) + .isEqualTo(testContent); + + // cleanup + server.stop(); + } + + @Test + public void should_return_default_resource_when_url_is_root() { + // given + int port = randomPort(); + final File outputDir = newOutputTestDirectory(); + final String defaultResource = "my_default.html"; + createFileWithContent(outputDir, defaultResource); + final String existingResource = "http://localhost:" + port; + + // when + final AsciidoctorHttpServer server = + new AsciidoctorHttpServer(Mockito.mock(Log.class), port, outputDir, defaultResource) + .start(); + + final HttpResponse response = doHttpGet(existingResource); + + // then + assertThat(response.getStatus()) + .isEqualTo(OK.code()); + assertThat(response.getMessage()) + .isEqualTo(OK.reasonPhrase()); + assertThat(response.getContent()) + .isEqualTo("Test content"); + + // cleanup + server.stop(); + } + + + private int randomPort() { + return 1000 + RANDOM.nextInt(8000); + } + + private HttpResponse doHttpGet(String url) { + return doHttp(url, "GET"); + } + + private HttpResponse doHttpPost(String url) { + return doHttp(url, "POST"); + } + + private HttpResponse doHttpHead(String url) { + return doHttp(url, "HEAD"); + } + + @SneakyThrows + private HttpResponse doHttp(String url, String method) { + try { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.setRequestMethod(method); + connection.connect(); + int status = connection.getResponseCode(); + + // due to HttpURLConnection returning FileNotFound exception on 404 + if (status == 404) + return HttpResponse.successful(status, connection.getResponseMessage(), null); + + InputStream is = (status >= 200 && status < 206) + ? connection.getInputStream() + : connection.getErrorStream(); + return HttpResponse.successful( + status, + connection.getResponseMessage(), + IOUtils.toString(is, StandardCharsets.UTF_8)); + } catch (Exception e) { + return HttpResponse.withError(e); + } + } + + @Value + static class HttpResponse { + Integer status; + String message; + String content; + + Exception error; + + static HttpResponse successful(int status, String message, String content) { + return new HttpResponse(status, message, content, null); + } + + static HttpResponse withError(Exception e) { + return new HttpResponse(null, null, null, e); + } + + static HttpResponse notFound() { + return successful(NOT_FOUND.code(), "Not Found", null); + } + } +}