Redirect Asciidoctor logging to Java Util Logging
ADD
asciidoctorj-core/src/main/java/org/asciidoctor/ast/Cursor.java
+package org.asciidoctor.ast;
+
+public interface Cursor {
+
+ /**
+ * @return The line number where the owning block begins.
+ */
+ int getLineNumber();
+
+ String getPath();
+
+ String getDir();
+
+ String getFile();
+
+}
\ No newline at end of file

ADD
asciidoctorj-core/src/main/java/org/asciidoctor/ast/impl/CursorImpl.java
+package org.asciidoctor.ast.impl;
+
+import org.asciidoctor.ast.Cursor;
+import org.asciidoctor.internal.RubyObjectWrapper;
+import org.jruby.runtime.builtin.IRubyObject;
+
+public class CursorImpl extends RubyObjectWrapper implements Cursor {
+
+ public CursorImpl(IRubyObject rubyNode) {
+ super(rubyNode);
+ }
+
+ @Override
+ public int getLineNumber() {
+ return getInt("lineno");
+ }
+
+ @Override
+ public String getPath() {
+ return getString("path");
+ }
+
+ @Override
+ public String getDir() {
+ return getString("dir");
+ }
+
+ @Override
+ public String getFile() {
+ return getString("file");
+ }
+
+ @Override
+ public String toString() {
+ return getString("to_s");
+ }
+}

MODIFY
asciidoctorj-core/src/main/java/org/asciidoctor/internal/JRubyAsciidoctorModuleFactory.java
asciidoctorj-core/src/main/java/org/asciidoctor/internal/JRubyAsciidoctorModuleFactory.java
package org.asciidoctor.internal;
-import java.io.InputStream;
-
import org.jruby.Ruby;
import org.jruby.RubyRuntimeAdapter;
import org.jruby.javasupport.JavaEmbedUtils;
+import java.io.InputStream;
+
class JRubyAsciidoctorModuleFactory {
// hack: to fix problem with copycss this should change in future.
@@ -22,6 +22,8 @@ class JRubyAsciidoctorModuleFactory {
String script = loadAsciidoctorRubyClass();
evaler.eval(runtime, script);
+ JavaLogger.install(runtime);
+
Object rfj = evaler.eval(runtime, "AsciidoctorModule.new()");
return RubyUtils.rubyToJava(runtime, (org.jruby.runtime.builtin.IRubyObject) rfj, AsciidoctorModule.class);

ADD
asciidoctorj-core/src/main/java/org/asciidoctor/internal/JavaLogger.java
+package org.asciidoctor.internal;
+
+import org.asciidoctor.ast.Cursor;
+import org.asciidoctor.ast.impl.CursorImpl;
+import org.jruby.Ruby;
+import org.jruby.RubyClass;
+import org.jruby.RubyHash;
+import org.jruby.RubyModule;
+import org.jruby.RubyObject;
+import org.jruby.anno.JRubyMethod;
+import org.jruby.runtime.Block;
+import org.jruby.runtime.Helpers;
+import org.jruby.runtime.ObjectAllocator;
+import org.jruby.runtime.ThreadContext;
+import org.jruby.runtime.backtrace.BacktraceElement;
+import org.jruby.runtime.builtin.IRubyObject;
+
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+public class JavaLogger extends RubyObject {
+
+
+ private static final int DEBUG = 0;
+ private static final int INFO = 1;
+ private static final int WARN = 2;
+ private static final int ERROR = 3;
+ private static final int FATAL = 4;
+ private static final int UNKNOWN = 5;
+
+ private static final String LOG_PROPERTY_SOURCE_LOCATION = "source_location";
+ private static final String LOG_PROPERTY_TEXT = "text";
+
+ static void install(final Ruby runtime) {
+
+ final RubyModule asciidoctorModule = runtime.getModule("Asciidoctor");
+ final RubyModule loggerManager = asciidoctorModule.defineOrGetModuleUnder("LoggerManager");
+ final RubyClass loggerBaseClass = asciidoctorModule.getClass("Logger");
+
+ final RubyClass loggerClass = asciidoctorModule
+ .defineOrGetModuleUnder("LoggerManager")
+ .defineClassUnder("JavaLogger", loggerBaseClass, new ObjectAllocator() {
+ @Override
+ public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
+ return new JavaLogger(runtime, klazz);
+ }
+ });
+
+ loggerClass.defineAnnotatedMethods(JavaLogger.class);
+
+ final IRubyObject logger = loggerClass.allocate();
+ logger.callMethod(runtime.getCurrentContext(), "initialize", runtime.getNil());
+ loggerManager.callMethod("logger=", logger);
+
+ }
+
+ private JavaLogger(final Ruby runtime, final RubyClass metaClass) {
+ super(runtime, metaClass);
+ }
+
+ @JRubyMethod(name = "initialize", required = 1, optional = 2)
+ public IRubyObject initialize(final ThreadContext threadContext, final IRubyObject[] args) {
+ return Helpers.invokeSuper(
+ threadContext,
+ this,
+ getMetaClass(),
+ "initialize",
+ args,
+ Block.NULL_BLOCK);
+ }
+
+ /**
+ * @param threadContext
+ * @param args
+ */
+ @JRubyMethod(name = "add", required = 1, optional = 2)
+ public IRubyObject add(final ThreadContext threadContext, final IRubyObject[] args, Block block) {
+ final IRubyObject rubyMessage;
+ final String progname;
+ if (args.length >= 2 && !args[1].isNil()) {
+ rubyMessage = args[1];
+ progname = args[2].asJavaString();
+ } else if (block.isGiven()) {
+ rubyMessage = block.yield(threadContext, getRuntime().getNil());
+ progname = this.getInstanceVariable("@progname").asJavaString();
+ } else {
+ rubyMessage = args[2];
+ progname = this.getInstanceVariable("@progname").asJavaString();
+ }
+ final Cursor cursor = getSourceLocation(rubyMessage);
+ final String message = formatMessage(cursor, rubyMessage);
+ final Level javaLogLevel = mapRubyLogLevel(args[0]);
+
+ final LogRecord record = createLogRecord(threadContext, javaLogLevel, cursor, message);
+
+ Logger logger = Logger.getLogger(progname);
+ logger.log(record);
+ return getRuntime().getNil();
+ }
+
+ private LogRecord createLogRecord(final ThreadContext threadContext,
+ final Level logLevel,
+ final Cursor cursor,
+ final String message) {
+ final LogRecord record = new LogRecord(logLevel, message);
+ BacktraceElement[] backtrace = threadContext.getBacktrace();
+ record.setSourceClassName(backtrace[2].getFilename());
+ record.setSourceMethodName(backtrace[2].getMethod());
+ record.setParameters(new Object[] { cursor });
+ return record;
+ }
+
+ private Level mapRubyLogLevel(IRubyObject arg) {
+ final Level javaLevel;
+ final int level = arg.convertToInteger().getIntValue();
+ switch (level) {
+ case DEBUG:
+ javaLevel = Level.FINEST;
+ break;
+ case INFO:
+ javaLevel = Level.INFO;
+ break;
+ case WARN:
+ javaLevel = Level.WARNING;
+ break;
+ case ERROR:
+ javaLevel = Level.SEVERE;
+ break;
+ case FATAL:
+ javaLevel = Level.SEVERE;
+ break;
+ case UNKNOWN:
+ default:
+ javaLevel = Level.INFO;
+ break;
+ }
+ return javaLevel;
+ }
+
+ private String formatMessage(final Cursor cursor, final IRubyObject msg) {
+ if (getRuntime().getString().equals(msg.getType())) {
+ return msg.asJavaString();
+ } else if (getRuntime().getHash().equals(msg.getType())) {
+ final RubyHash hash = (RubyHash) msg;
+ final String text = Objects.toString(hash.get(getRuntime().newSymbol(LOG_PROPERTY_TEXT)));
+ return cursor != null ? cursor.toString() + ": " + text : text;
+ }
+ throw new IllegalArgumentException(Objects.toString(msg));
+ }
+
+ private Cursor getSourceLocation(IRubyObject msg) {
+ if (getRuntime().getHash().equals(msg.getType())) {
+ final RubyHash hash = (RubyHash) msg;
+ final Object sourceLocation = hash.get(getRuntime().newSymbol(LOG_PROPERTY_SOURCE_LOCATION));
+ return new CursorImpl((IRubyObject) sourceLocation);
+ }
+ return null;
+ }
+}

ADD
asciidoctorj-core/src/main/java/org/asciidoctor/internal/RubyObjectWrapper.java
+package org.asciidoctor.internal;
+
+import org.jruby.Ruby;
+import org.jruby.RubyArray;
+import org.jruby.RubyBoolean;
+import org.jruby.RubyFixnum;
+import org.jruby.RubyNil;
+import org.jruby.RubyNumeric;
+import org.jruby.RubyString;
+import org.jruby.RubySymbol;
+import org.jruby.javasupport.JavaEmbedUtils;
+import org.jruby.runtime.ThreadContext;
+import org.jruby.runtime.builtin.IRubyObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class RubyObjectWrapper {
+
+ protected Ruby runtime;
+ protected IRubyObject rubyNode;
+
+ public RubyObjectWrapper(IRubyObject rubyNode) {
+ this.rubyNode = rubyNode;
+ this.runtime = rubyNode.getRuntime();
+ }
+
+ public IRubyObject getRubyObject() {
+ return rubyNode;
+ }
+
+ protected Ruby getRuntime() {
+ return runtime;
+ }
+
+ public String getString(String propertyName, Object... args) {
+ IRubyObject result = getRubyProperty(propertyName, args);
+
+ if (result instanceof RubyNil) {
+ return null;
+ } else if (result instanceof RubySymbol) {
+ return ((RubySymbol) result).asJavaString();
+ } else {
+ return ((RubyString) result).asJavaString();
+ }
+ }
+
+ public void setString(String propertyName, String value) {
+ if (value == null) {
+ setRubyProperty(propertyName, runtime.getNil());
+ } else {
+ setRubyProperty(propertyName, runtime.newString(value));
+ }
+ }
+
+ public String getSymbol(String propertyName, Object... args) {
+ IRubyObject result = getRubyProperty(propertyName, args);
+
+ if (result instanceof RubyNil) {
+ return null;
+ }
+ return ((RubySymbol) result).asJavaString();
+ }
+
+ public void setSymbol(String propertyName, String value) {
+ if (value == null) {
+ setRubyProperty(propertyName, runtime.getNil());
+ } else {
+ setRubyProperty(propertyName, runtime.newSymbol(value));
+ }
+ }
+
+ public boolean getBoolean(String propertyName, Object... args) {
+ IRubyObject result = getRubyProperty(propertyName, args);
+ if (result instanceof RubyNil) {
+ return false;
+ } else {
+ return ((RubyBoolean) result).isTrue();
+ }
+ }
+
+ public int getInt(String propertyName, Object... args) {
+ IRubyObject result = getRubyProperty(propertyName, args);
+ if (result instanceof RubyNil) {
+ return 0;
+ } else {
+ return (int) ((RubyNumeric) result).getLongValue();
+ }
+ }
+
+ public <T> List<T> getList(String propertyName, Class<T> elementClass, Object... args) {
+ IRubyObject result = getRubyProperty(propertyName, args);
+ if (result instanceof RubyNil) {
+ return null;
+ } else {
+ List<T> ret = new ArrayList<T>();
+ RubyArray array = (RubyArray) result;
+ for (int i = 0; i < array.size(); i++) {
+ ret.add(RubyUtils.rubyToJava(runtime, array.at(RubyFixnum.newFixnum(runtime, i)), elementClass));
+ }
+ return ret;
+ }
+ }
+
+
+ public IRubyObject getRubyProperty(String propertyName, Object... args) {
+ ThreadContext threadContext = runtime.getThreadService().getCurrentContext();
+
+ IRubyObject result = null;
+ if (propertyName.startsWith("@")) {
+ if (args != null && args.length > 0) {
+ throw new IllegalArgumentException("No args allowed for direct field access");
+ }
+ result = rubyNode.getInstanceVariables().getInstanceVariable(propertyName);
+ } else {
+ if (args == null) {
+ result = rubyNode.callMethod(threadContext, propertyName);
+ } else {
+ IRubyObject[] rubyArgs = new IRubyObject[args.length];
+ for (int i = 0; i < args.length; i++) {
+ if (args[i] instanceof RubyObjectWrapper) {
+ rubyArgs[i] = ((RubyObjectWrapper)args[i]).getRubyObject();
+ } else {
+ rubyArgs[i] = JavaEmbedUtils.javaToRuby(runtime, args[i]);
+ }
+ }
+ result = rubyNode.callMethod(threadContext, propertyName, rubyArgs);
+ }
+ }
+ return result;
+ }
+
+ public void setRubyProperty(String propertyName, IRubyObject arg) {
+ ThreadContext threadContext = runtime.getThreadService().getCurrentContext();
+
+ IRubyObject result = null;
+ if (propertyName.startsWith("@")) {
+ rubyNode.getInstanceVariables().setInstanceVariable(propertyName, arg);
+ } else {
+ if (arg == null) {
+ rubyNode.callMethod(threadContext, propertyName + "=", runtime.getNil());
+ } else {
+ rubyNode.callMethod(threadContext, propertyName + "=", arg);
+ }
+ }
+ }
+
+ public Object getProperty(String propertyName, Object... args) {
+ return toJava(getRubyProperty(propertyName, args));
+ }
+
+ public Object toJava(IRubyObject rubyObject) {
+ return JavaEmbedUtils.rubyToJava(rubyObject);
+ }
+
+ public <T> T toJava(IRubyObject rubyObject, Class<T> targetClass) {
+ return (T) JavaEmbedUtils.rubyToJava(runtime, rubyObject, targetClass);
+ }
+
+
+}
\ No newline at end of file

ADD
asciidoctorj-core/src/test/java/org/asciidoctor/MemoryLogHandler.java
+package org.asciidoctor;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Handler;
+import java.util.logging.LogRecord;
+
+public class MemoryLogHandler extends Handler {
+
+ private List<LogRecord> logRecords = new ArrayList<>();
+
+ @Override
+ public void publish(LogRecord record) {
+ logRecords.add(record);
+ }
+
+ @Override
+ public void flush() {
+ }
+
+ @Override
+ public void close() throws SecurityException {
+ logRecords.clear();
+ }
+
+ public List<LogRecord> getLogRecords() {
+ return logRecords;
+ }
+}
+

MODIFY
asciidoctorj-core/src/test/java/org/asciidoctor/WhenAsciidoctorLogsToConsole.java
asciidoctorj-core/src/test/java/org/asciidoctor/WhenAsciidoctorLogsToConsole.java
package org.asciidoctor;
+import org.asciidoctor.ast.Cursor;
import org.asciidoctor.internal.JRubyAsciidoctor;
import org.asciidoctor.util.ClasspathResources;
+import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
+import java.io.IOException;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
import static org.asciidoctor.OptionsBuilder.options;
+import static org.hamcrest.Matchers.both;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
public class WhenAsciidoctorLogsToConsole {
@@ -20,8 +31,14 @@ public class WhenAsciidoctorLogsToConsole {
private Asciidoctor asciidoctor = JRubyAsciidoctor.create();
+ @After
+ public void cleanup() throws IOException {
+ LogManager.getLogManager().readConfiguration();
+ }
+
@Test
- public void shouldBeRedirectToAsciidoctorJLoggerSystem() {
+ public void shouldBeRedirectToAsciidoctorJLoggerSystem() throws Exception {
+ final MemoryLogHandler memoryLogHandler = registerMemoryLogHandler();
File inputFile = classpath.getResource("documentwithnotexistingfile.adoc");
String renderContent = asciidoctor.renderFile(inputFile, options()
@@ -29,6 +46,23 @@ public class WhenAsciidoctorLogsToConsole {
File expectedFile = new File(inputFile.getParent(), "documentwithnotexistingfile.html");
expectedFile.delete();
+
+ assertEquals(1, memoryLogHandler.getLogRecords().size());
+ assertThat(memoryLogHandler.getLogRecords().get(0).getMessage(),
+ both(containsString("include file not found"))
+ .and(containsString("documentwithnotexistingfile.adoc: line 3")));
+ assertThat(memoryLogHandler.getLogRecords().get(0).getParameters()[0], instanceOf(Cursor.class));
+ final Cursor cursor = (Cursor) memoryLogHandler.getLogRecords().get(0).getParameters()[0];
+ assertThat(cursor.getDir(), is(inputFile.getParent()));
+ assertThat(cursor.getFile(), is(inputFile.getName()));
+ assertThat(cursor.getLineNumber(), is(3));
+ }
+
+ private MemoryLogHandler registerMemoryLogHandler() {
+ final Logger logger = Logger.getLogger("asciidoctor");
+ final MemoryLogHandler handler = new MemoryLogHandler();
+ logger.addHandler(handler);
+ return handler;
}
}

MODIFY
build.gradle
build.gradle
@@ -45,7 +45,7 @@ ext {
xmlMatchersVersion = '1.0-RC1'
// gem versions
- asciidoctorGemVersion = project.hasProperty('asciidoctorGemVersion') ? project.asciidoctorGemVersion : '1.5.6.1'
+ asciidoctorGemVersion = project.hasProperty('asciidoctorGemVersion') ? project.asciidoctorGemVersion : '1.5.7'
asciidoctorEpub3GemVersion = project(':asciidoctorj-epub3').version.replace('-', '.')
asciidoctorDiagramGemVersion = project(':asciidoctorj-diagram').version.replace('-', '.')
@@ -83,7 +83,7 @@ subprojects {
// NOTE sourceCompatibility & targetCompatibility are set in gradle.properties to meet requirements of Gradle
// Must redefine here to work around a bug in the Eclipse plugin
- sourceCompatibility = targetCompatibility = JavaVersion.VERSION_1_6
+ sourceCompatibility = targetCompatibility = JavaVersion.VERSION_1_7
plugins.withType(JavaPlugin) {
project.tasks.withType(JavaCompile) { task ->

MODIFY
gradle.properties
gradle.properties
version=1.5.7-SNAPSHOT
-sourceCompatibility=1.6
-targetCompatibility=1.6
+sourceCompatibility=1.7
+targetCompatibility=1.7

Redirect Asciidoctor logging to Java Util Logging