Skip to content

Commit

Permalink
auto-refresh only modified and created sources
Browse files Browse the repository at this point in the history
  • Loading branch information
abelsromero committed Jul 27, 2020
1 parent c534bc6 commit d08ed76
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 149 deletions.
172 changes: 45 additions & 127 deletions src/main/java/org/asciidoctor/maven/AsciidoctorRefreshMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@

package org.asciidoctor.maven;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.*;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.filefilter.NameFileFilter;
import org.apache.commons.io.filefilter.RegexFileFilter;
import org.apache.commons.io.monitor.FileAlterationListener;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import org.apache.commons.io.monitor.FileAlterationMonitor;
Expand All @@ -23,15 +25,13 @@
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.asciidoctor.Asciidoctor;

import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.Scanner;
import java.util.StringJoiner;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.TimeUnit;

@Mojo(name = "auto-refresh")
public class AsciidoctorRefreshMojo extends AsciidoctorMojo {
Expand All @@ -41,48 +41,30 @@ public class AsciidoctorRefreshMojo extends AsciidoctorMojo {
@Parameter(property = PREFIX + "interval")
protected int interval = 2000; // 2s

private Future<Asciidoctor> asciidoctor = null;
private Collection<FileAlterationMonitor> monitors = null;

private final AtomicBoolean needsUpdate = new AtomicBoolean(false);
private ScheduledExecutorService updaterScheduler = null;

@Override
public void execute() throws MojoExecutionException, MojoFailureException {
// this is long because of JRuby startup
createAsciidoctor();

startPolling();
startUpdater();

doWork();

stopUpdater();
stopMonitor();
}

private void stopUpdater() {
if (updaterScheduler != null) {
updaterScheduler.shutdown();
}
}

private void startUpdater() {
updaterScheduler = Executors.newScheduledThreadPool(1);

// we prevent refreshing more often than all 200ms and we refresh at least once/s
// NOTE1: it is intended to avoid too much time space between file polling and re-converting
// NOTE2: if nothing to refresh it does nothing so all is fine
updaterScheduler.scheduleAtFixedRate(new Updater(needsUpdate, this), 0, Math.min(1000, Math.max(200, interval / 2)), TimeUnit.MILLISECONDS);
stopMonitors();
}

protected void doWork() throws MojoFailureException, MojoExecutionException {
getLog().info("Converted doc(s) in " + executeAndReturnDuration() + "ms");
long timeInMillis = timed(() -> {
try {
processAllSources();
} catch (MojoExecutionException e) {
getLog().error(e);
}
});
getLog().info("Converted document(s) in " + timeInMillis + "ms");
doWait();
}

protected void doWait() {
getLog().info("Type [exit|quit] to exit and [refresh] to force a manual re-conversion.");
protected void doWait() throws MojoExecutionException, MojoFailureException {
showWaitMessage();

String line;
final Scanner scanner = new Scanner(System.in);
Expand All @@ -93,14 +75,18 @@ protected void doWait() {
}

if ("refresh".equalsIgnoreCase(line)) {
doExecute();
doWork();
} else {
getLog().warn("'" + line + "' not understood, available commands are [quit, exit, refresh].");
}
}
}

private void stopMonitor() throws MojoExecutionException {
private void showWaitMessage() {
getLog().info("Type [exit|quit] to exit and [refresh] to force a manual re-conversion.");
}

private void stopMonitors() throws MojoExecutionException {
if (monitors != null) {
for (final FileAlterationMonitor monitor : monitors) {
try {
Expand All @@ -112,36 +98,11 @@ private void stopMonitor() throws MojoExecutionException {
}
}

protected synchronized void doExecute() {
if (!ensureOutputExists())
getLog().error("Can't create " + outputDirectory.getPath());

// delete only content files, resources are synchronized so normally up to date
for (final File f : FileUtils.listFiles(outputDirectory, new RegexFileFilter(ASCIIDOC_REG_EXP_EXTENSION), TrueFileFilter.INSTANCE)) {
FileUtils.deleteQuietly(f);
}

try {
getLog().info("Re-converted doc(s) in " + executeAndReturnDuration() + "ms");
} catch (final MojoExecutionException e) {
getLog().error(e);
} catch (final MojoFailureException e) {
getLog().error(e);
}
}

protected long executeAndReturnDuration() throws MojoExecutionException, MojoFailureException {
final long start = System.nanoTime();
super.execute();
final long end = System.nanoTime();
return TimeUnit.NANOSECONDS.toMillis(end - start);
}

private void startPolling() throws MojoExecutionException {

{ // content monitor
final FileAlterationObserver observer = new FileAlterationObserver(sourceDirectory, buildFileFilter());
final FileAlterationListener listener = new AtomicFlagFileAlterationListenerAdaptor(needsUpdate, getLog());
final FileAlterationListener listener = new AsciidoctorConverterFileAlterationListenerAdaptor(this, () -> showWaitMessage(), getLog());
final FileAlterationMonitor monitor = new FileAlterationMonitor(interval);

observer.addListener(listener);
Expand Down Expand Up @@ -174,89 +135,46 @@ private IOFileFilter buildFileFilter() {
return new RegexFileFilter(ASCIIDOC_REG_EXP_EXTENSION);
}

private void createAsciidoctor() {
final ExecutorService es = Executors.newSingleThreadExecutor();
asciidoctor = es.submit(() -> Asciidoctor.Factory.create());
es.shutdown();
}

private static class Updater implements Runnable {

private final AtomicBoolean run;
private final AsciidoctorRefreshMojo mojo;

private Updater(final AtomicBoolean run, final AsciidoctorRefreshMojo mojo) {
this.run = run;
this.mojo = mojo;
}

@Override
public void run() {
if (run.get()) {
run.set(false);
mojo.doExecute();
}
}
}

private static class AsciidoctorConverterFileAlterationListenerAdaptor extends FileAlterationListenerAdaptor {
private class AsciidoctorConverterFileAlterationListenerAdaptor extends FileAlterationListenerAdaptor {

private final AsciidoctorMojo mojo;
private final AtomicBoolean needsUpdate;
private final Runnable postAction;
private final Log log;

private AsciidoctorConverterFileAlterationListenerAdaptor(AsciidoctorMojo config, AtomicBoolean needsUpdate, Log log) {
private AsciidoctorConverterFileAlterationListenerAdaptor(AsciidoctorMojo config, Runnable postAction, Log log) {
this.mojo = config;
this.needsUpdate = needsUpdate;
this.postAction = postAction;
this.log = log;
}

@Override
public void onFileCreate(final File file) {
log.info("File " + file.getAbsolutePath() + " created.");
// mojo.convert(s);
needsUpdate.set(true);
processFile(file, "created");
}

@Override
public void onFileChange(final File file) {
log.info("File " + file.getAbsolutePath() + " updated.");
needsUpdate.set(true);
processFile(file, "updated");
}

@Override
public void onFileDelete(final File file) {
log.info("File " + file.getAbsolutePath() + " deleted.");
needsUpdate.set(true);
private synchronized void processFile(File file, String actionName) {
log.info(String.format("Source file %s %s", file.getAbsolutePath(), actionName));
long timeInMillis = timed(() -> {
try {
mojo.processSources(Collections.singletonList(file));
} catch (MojoExecutionException e) {
log.error(e);
}
});
getLog().info("Converted document in " + timeInMillis + "ms");
postAction.run();
}
}

private static class AtomicFlagFileAlterationListenerAdaptor extends FileAlterationListenerAdaptor {

private final AtomicBoolean needsUpdate;
private final Log log;

private AtomicFlagFileAlterationListenerAdaptor(AtomicBoolean needsUpdate, Log log) {
this.needsUpdate = needsUpdate;
this.log = log;
}

@Override
public void onFileCreate(final File file) {
log.info("File " + file.getAbsolutePath() + " created.");
needsUpdate.set(true);
}

@Override
public void onFileChange(final File file) {
log.info("File " + file.getAbsolutePath() + " updated.");
needsUpdate.set(true);
}

@Override
public void onFileDelete(final File file) {
log.info("File " + file.getAbsolutePath() + " deleted.");
needsUpdate.set(true);
}
public long timed(Runnable runnable) {
final long start = System.nanoTime();
runnable.run();
final long end = System.nanoTime();
return TimeUnit.NANOSECONDS.toMillis(end - start);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.asciidoctor.maven.test


import org.asciidoctor.maven.AsciidoctorRefreshMojo
import org.asciidoctor.maven.test.io.DoubleOuputStream
import org.asciidoctor.maven.test.io.PrefilledInputStream
Expand All @@ -18,7 +19,7 @@ class AsciidoctorRefreshMojoTest extends Specification {

def "auto convert when source updated"() {
setup:
def srcDir = new File('target/test-classes/src/asciidoctor-refresh')
File srcDir = new File('target/test-classes/src/asciidoctor-refresh')
srcDir.mkdirs()
File outputDir = newOutputTestDirectory('refresh-mojo')

Expand All @@ -38,48 +39,48 @@ class AsciidoctorRefreshMojoTest extends Specification {
if (content.exists())
content.delete()

content.withWriter{ it <<
'''= Document Title
This is test, only a test.'''.stripIndent() }
content.withWriter {
it <<
'''= Document Title
This is test, only a test.'''.stripIndent()
}

def target = new File(outputDir, content.name.replace('.asciidoc', '.html'))

def mojo = new AsciidoctorRefreshMojo()
mojo.backend = 'html'
mojo.sourceDirectory = srcDir
mojo.outputDirectory = outputDir
def mojoThread = new Thread(new Runnable() {
@Override
void run() {
mojo.execute()
println 'end'
}
def mojoThread = new Thread({
mojo.execute()
println 'end'
})
mojoThread.start()

while (!new String(newOut.toByteArray()).contains('Converted')) {
Thread.sleep(200)
while (!new String(newOut.toByteArray()).contains('Converted document(s) in')) {
Thread.sleep(300)
}

assert target.text.contains('This is test, only a test')

when:
content.withWriter{ it <<
'''= Document Title
Wow, this will be auto refreshed!'''.stripIndent() }
content.withWriter {
it <<
'''= Document Title
Wow, this will be auto refreshed!'''.stripIndent()
}

then:
while (!new String(newOut.toByteArray()).contains('Re-converted ')) {
Thread.sleep 500
while (!new String(newOut.toByteArray()).contains("Converted document in")) {
Thread.sleep(300)
}

assert target.text.contains('Wow, this will be auto refreshed')

cleanup:
System.setOut(originalOut)
inputLatch.countDown()
System.setIn(originalIn)

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.asciidoctor.maven.test.io
import java.util.concurrent.CountDownLatch

class PrefilledInputStream extends ByteArrayInputStream {

final CountDownLatch latch

PrefilledInputStream(final byte[] buf, final CountDownLatch latch) {
Expand All @@ -11,7 +12,7 @@ class PrefilledInputStream extends ByteArrayInputStream {
}

@Override
public synchronized int read(final byte[] b, final int off, final int len) {
synchronized int read(final byte[] b, final int off, final int len) {
latch.await()
return super.read(b, off, len)
}
Expand Down

0 comments on commit d08ed76

Please sign in to comment.