Skip to content

Commit

Permalink
closes #468: adds life refresh in http mojo (#483)
Browse files Browse the repository at this point in the history
  • Loading branch information
abelsromero committed Sep 6, 2020
1 parent 14e0635 commit 08fc4e0
Show file tree
Hide file tree
Showing 8 changed files with 575 additions and 93 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Expand Up @@ -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 <resources> 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::

Expand Down
68 changes: 67 additions & 1 deletion README.adoc
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 <<auto-refresh-goal,auto-refresh>> 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
----
<plugin>
...
<executions>
<execution>
<id>output-html</id>
<phase>generate-resources</phase> <!--1-->
<goals>
<goal>http</goal> <!--2-->
</goals>
<configuration> <!--3-->
<port>8080</port>
<attributes>
<toc/>
<linkcss>false</linkcss>
<source-highlighter>coderay</source-highlighter>
</attributes>
</configuration>
</execution>
</executions>
</plugin>
----
<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 <<auto-refresh-goal,auto-refresh>> 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 <<auto-refresh-goal-config-note,note>>).
* `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
Expand Down
58 changes: 4 additions & 54 deletions 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 {
Expand All @@ -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<String, Object> 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("</body>", "<script>setTimeout(\"location.reload(true);\"," + autoReloadInterval + ");</script>\n</body>");
stopMonitors();
}

public String getHome() {
Expand Down
Expand Up @@ -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 {
Expand All @@ -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<File> sourceDirectoryCandidate = findSourceDirectory(sourceDirectory, project.getBasedir());
Expand Down
82 changes: 48 additions & 34 deletions 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<FullHttpRequest> {

private static final String HTML_MEDIA_TYPE = "text/html";
public static final String HTML_EXTENSION = ".html";

Expand All @@ -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("<html><body>Only GET method allowed</body></html>", 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("<body><html>File not found: " + file.getPath() + "<body></html>", 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("<body><html>File not found: " + file.getPath() + "<body></html>", 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("</body>", "<script src=\"http://livejs.com/live.js#html\"></script></body>");
}

private void send(final ChannelHandlerContext ctx, final DefaultFullHttpResponse response) {
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
Expand All @@ -80,18 +95,17 @@ 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) {
return path + HTML_EXTENSION;
}

private static String mediaType(final String name) {
if (name.endsWith(".html")) {
return HTML_MEDIA_TYPE;
}
if (name.endsWith(".js")) {
return "text/javascript";
}
Expand All @@ -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";
}
}
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit 08fc4e0

Please sign in to comment.