diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..6da8cf94
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+- package-ecosystem: maven
+ directory: "/"
+ schedule:
+ interval: "daily"
+ open-pull-requests-limit: 10
diff --git a/.github/workflows/apidoc.yml b/.github/workflows/apidoc.yml
new file mode 100644
index 00000000..01c1334d
--- /dev/null
+++ b/.github/workflows/apidoc.yml
@@ -0,0 +1,49 @@
+# Simple workflow for deploying static content to GitHub Pages
+name: Deploy static content to Pages
+
+on:
+ # Runs on pushes targeting the default branch
+ push:
+ branches: ["main"]
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow one concurrent deployment
+concurrency:
+ group: "pages"
+ cancel-in-progress: true
+
+jobs:
+ # Single deploy job since we're just deploying
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Setup Pages
+ uses: actions/configure-pages@v2
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ - name: mvn install
+ run: ./mvnw --batch-mode --no-transfer-progress -Pdoc clean install -DskipTests=true
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v1
+ with:
+ # Upload entire repository
+ path: 'target/site/apidocs'
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v1
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
new file mode 100644
index 00000000..521b128d
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -0,0 +1,39 @@
+name: Build with Maven
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+ workflow_dispatch:
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+ env:
+ BUILD_NUMBER: "${{github.run_number}}"
+ MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode --no-transfer-progress"
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ - name: Cache local Maven repository
+ uses: actions/cache@v2
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+ - name: Build and Test with Maven
+ run: ./mvnw $MAVEN_CLI_OPTS clean verify
+ - name: Upload Test Results
+ uses: actions/upload-artifact@v3
+ if: always()
+ with:
+ name: test-results
+ path: '**/target/surefire-reports/TEST-*.xml'
diff --git a/.github/workflows/test-report.yml b/.github/workflows/test-report.yml
new file mode 100644
index 00000000..66bba612
--- /dev/null
+++ b/.github/workflows/test-report.yml
@@ -0,0 +1,16 @@
+name: 'Test Report'
+on:
+ workflow_run:
+ workflows: ['Build with Maven']
+ types:
+ - completed
+jobs:
+ report:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: dorny/test-reporter@v1
+ with:
+ artifact: test-results
+ name: Maven Surefire Tests
+ path: '**/target/surefire-reports/TEST-*.xml'
+ reporter: java-junit
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..978d5ec7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,45 @@
+*.class
+
+# Package Files #
+*.jar
+*.war
+*.ear
+target
+.classpath
+.project
+.settings
+.factorypath
+dependency-reduced-pom.xml
+
+# OSX
+*.DS_Store
+
+# IntelliJ IDE Project Files #
+*.iml
+*.idea
+*.versionsBackup
+pom.xml.versionsBackup
+jacoco.exec
+.*.md.html
+node
+dump
+TODO
+.interp
+tmp
+checkstyle
+#javadoc
+*.mv.db
+versions
+out
+node
+node_modules
+build
+.gradle
+
+# Emacs
+
+\#*\#
+.\#*
+
+#VS code
+.vscode
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..ac184013
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/core/pom.xml b/core/pom.xml
new file mode 100644
index 00000000..812d2c2f
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,17 @@
+
+ 4.0.0
+
+ io.jstach.rainbowgum
+ rainbowgum-maven-parent
+ 0.1.3-SNAPSHOT
+
+ rainbowgum-core
+
+
+ org.fusesource.jansi
+ jansi
+
+
+
\ No newline at end of file
diff --git a/core/src/main/java/io/jstach/rainbowgum/LogAppender.java b/core/src/main/java/io/jstach/rainbowgum/LogAppender.java
new file mode 100644
index 00000000..65b295c1
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/LogAppender.java
@@ -0,0 +1,20 @@
+package io.jstach.rainbowgum;
+
+public interface LogAppender {
+
+ public void append(
+ LogEvent event);
+
+ public static LogAppender of(LogOutput output, LogFormatter formatter) {
+ return new DefaultLogAppender(output, formatter);
+ }
+}
+
+record DefaultLogAppender(LogOutput output, LogFormatter formatter) implements LogAppender {
+ @Override
+ public void append(
+ LogEvent event) {
+ formatter.format(output, event);
+
+ }
+}
diff --git a/core/src/main/java/io/jstach/rainbowgum/LogConfig.java b/core/src/main/java/io/jstach/rainbowgum/LogConfig.java
new file mode 100644
index 00000000..b758409f
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/LogConfig.java
@@ -0,0 +1,49 @@
+package io.jstach.rainbowgum;
+
+import java.lang.System.Logger.Level;
+import java.util.Map;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+public interface LogConfig {
+
+ @Nullable
+ String property(String key);
+
+ default LogEncoder defaultOutput() {
+ return LogEncoder.of(System.out);
+ }
+
+ default String hostName() {
+ String hostName = property("HOSTNAME");
+ if (hostName == null) {
+ return "localhost";
+ }
+ return hostName;
+ }
+
+ default Map headers() {
+ return Map.of();
+ }
+
+ default @Nullable Level logLevel(String name) {
+ String s = property("log." + name);
+ if (s == null || s.isBlank()) {
+ return null;
+ }
+ return Level.valueOf(s);
+ }
+
+ public static LogConfig of(Function propertySupplier) {
+ return new LogConfig() {
+
+ @Override
+ public @Nullable String property(
+ String key) {
+ return "rainbowgum." + key;
+ }
+ };
+ }
+}
+
diff --git a/core/src/main/java/io/jstach/rainbowgum/LogEncoder.java b/core/src/main/java/io/jstach/rainbowgum/LogEncoder.java
new file mode 100644
index 00000000..a1ffc6b1
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/LogEncoder.java
@@ -0,0 +1,48 @@
+package io.jstach.rainbowgum;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+
+public interface LogEncoder extends LogOutput {
+
+ @Override
+ default void append(
+ String s) {
+ encode(s.getBytes(StandardCharsets.UTF_8));
+ }
+
+ public void encode(byte[] bytes);
+
+ public void encode(byte[] bytes, int off, int len);
+
+ public static LogEncoder of(OutputStream out) {
+ return new LogEncoder() {
+
+ @Override
+ public void encode(
+ byte[] bytes,
+ int off,
+ int len) {
+ try {
+ out.write(bytes, off, len);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public void encode(
+ byte[] bytes) {
+ try {
+ out.write(bytes);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+
+ }
+ };
+ }
+
+}
diff --git a/core/src/main/java/io/jstach/rainbowgum/LogEvent.java b/core/src/main/java/io/jstach/rainbowgum/LogEvent.java
new file mode 100644
index 00000000..13c04d18
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/LogEvent.java
@@ -0,0 +1,59 @@
+package io.jstach.rainbowgum;
+
+import java.time.Instant;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+public record LogEvent(
+ Instant timeStamp,
+ String threadName,
+ long threadId,
+ System.Logger.Level level,
+ String loggerName,
+ String loggerShortName,
+ String formattedMessage,
+ Map keyValues,
+ @Nullable Throwable throwable) {
+
+ public @Nullable Throwable getThrowable() {
+ return throwable();
+ }
+
+ @SuppressWarnings({ "null", "exports" })
+ public Map getKeyValues() {
+ return keyValues;
+ }
+
+ public static LogEvent of(
+ System.Logger.Level level,
+ String loggerName,
+ String loggerShortName,
+ String formattedMessage,
+ Map keyValues,
+ @Nullable Throwable throwable) {
+ Instant timeStamp = Instant.now();
+ Thread currentThread = Thread.currentThread();
+ String threadName = currentThread.getName();
+ long threadId = currentThread.getId();
+
+ return new LogEvent(
+ timeStamp,
+ threadName,
+ threadId,
+ level,
+ loggerName,
+ loggerName,
+ formattedMessage,
+ keyValues,
+ throwable);
+ }
+
+ public static LogEvent of(
+ System.Logger.Level level,
+ String loggerName,
+ String formattedMessage,
+ @Nullable Throwable throwable) {
+ return of(level, loggerName, loggerName, formattedMessage, Map.of(), throwable);
+ }
+}
diff --git a/core/src/main/java/io/jstach/rainbowgum/LogFormatter.java b/core/src/main/java/io/jstach/rainbowgum/LogFormatter.java
new file mode 100644
index 00000000..5f8f476f
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/LogFormatter.java
@@ -0,0 +1,55 @@
+package io.jstach.rainbowgum;
+
+import java.lang.System.Logger.Level;
+import java.time.Instant;
+
+import io.jstach.rainbowgum.LogFormatter.LevelFormatter;
+import io.jstach.rainbowgum.LogFormatter.ThrowableFormatter;
+
+public interface LogFormatter {
+
+ public void format(LogOutput output, LogEvent event);
+
+ public interface LevelFormatter {
+ String format(Level level);
+ public static LevelFormatter of() {
+ return DefaultLevelFormatter.INSTANT;
+ }
+ }
+
+ public interface InstantFormatter {
+ String format(Instant instant);
+ }
+
+ public interface ThrowableFormatter {
+ void format(LogOutput output, Throwable throwable);
+ }
+
+}
+enum DefaultThrowableFormatter implements ThrowableFormatter {
+ INSTANT;
+ @Override
+ public void format(
+ LogOutput output,
+ Throwable throwable) {
+ throwable.printStackTrace(output.asWriter());
+ }
+}
+enum DefaultLevelFormatter implements LevelFormatter {
+ INSTANT;
+
+ @Override
+ public String format(
+ Level level) {
+ return switch(level) {
+ case DEBUG -> "DEBUG";
+ case ALL -> "ERROR";
+ case ERROR -> "ERROR";
+ case INFO -> "INFO";
+ case OFF -> "TRACE";
+ case TRACE -> "TRACE";
+ case WARNING -> "WARN";
+ };
+ }
+
+}
diff --git a/core/src/main/java/io/jstach/rainbowgum/LogOutput.java b/core/src/main/java/io/jstach/rainbowgum/LogOutput.java
new file mode 100644
index 00000000..e085701a
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/LogOutput.java
@@ -0,0 +1,54 @@
+package io.jstach.rainbowgum;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.Writer;
+
+public interface LogOutput {
+
+ void append(String s);
+
+ default PrintWriter asWriter() {
+ return toWriter(this);
+ }
+
+ public static PrintWriter toWriter(LogOutput out) {
+ if (out instanceof PrintWriter w) {
+ return w;
+ }
+ var writer = new Writer() {
+
+ @Override
+ public void write(
+ String str)
+ throws IOException {
+ out.append(str);
+ }
+
+ @Override
+ public void write(
+ char[] cbuf,
+ int off,
+ int len)
+ throws IOException {
+ out.append(String.valueOf(cbuf, off, len));
+ }
+
+ @Override
+ public void flush()
+ throws IOException {
+
+ }
+
+ @Override
+ public void close()
+ throws IOException {
+
+ }
+ };
+ return new PrintWriter(writer);
+ }
+
+}
+
diff --git a/core/src/main/java/io/jstach/rainbowgum/LogPlugin.java b/core/src/main/java/io/jstach/rainbowgum/LogPlugin.java
new file mode 100644
index 00000000..d37d368f
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/LogPlugin.java
@@ -0,0 +1,20 @@
+package io.jstach.rainbowgum;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.List;
+
+public interface LogPlugin {
+
+ default List getNames() {
+ return List.of(requireNonNull(getClass().getCanonicalName()));
+ }
+
+ default boolean isEnabledByDefault() {
+ return true;
+ }
+
+ public List createLogAppenders(
+ LogConfig config);
+
+}
diff --git a/core/src/main/java/io/jstach/rainbowgum/LogRouter.java b/core/src/main/java/io/jstach/rainbowgum/LogRouter.java
new file mode 100644
index 00000000..2e9e21d9
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/LogRouter.java
@@ -0,0 +1,125 @@
+package io.jstach.rainbowgum;
+
+import java.lang.System.Logger.Level;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+public interface LogRouter {
+
+ boolean isEnabled(
+ String loggerName,
+ java.lang.System.Logger.Level level);
+
+ default void log(
+ String loggerName,
+ java.lang.System.Logger.Level level,
+ String message,
+ @Nullable Throwable cause) {
+ log(LogEvent.of(level, loggerName, message, cause));
+ }
+
+ void log(
+ LogEvent event);
+
+ public static void setRouter(LogRouter router) {
+ GlobalLogRouter.INSTANCE.drain(router);
+ }
+
+ public static LogRouter of() {
+ return GlobalLogRouter.INSTANCE;
+ }
+
+ public static LogRouter of(List extends LogAppender> appenders) {
+ return new DefaultLogRouter(appenders);
+ }
+}
+
+class DefaultLogRouter implements LogRouter {
+
+ private final List extends LogAppender> logAppenders;
+
+ public DefaultLogRouter(
+ List extends LogAppender> logAppenders) {
+ super();
+ this.logAppenders = logAppenders;
+ }
+
+ @Override
+ public boolean isEnabled(
+ String loggerName,
+ Level level) {
+ return true;
+ }
+
+
+ @Override
+ public void log(
+ LogEvent event) {
+ for (var a : logAppenders) {
+ a.append(event);
+ }
+ }
+
+}
+enum GlobalLogRouter implements LogRouter {
+ INSTANCE;
+
+ private final ConcurrentLinkedQueue events = new ConcurrentLinkedQueue<>();
+ private volatile @Nullable LogRouter delegate = null;
+
+ public boolean isEnabled(
+ String loggerName,
+ java.lang.System.Logger.Level level) {
+ LogRouter d = delegate;
+ if (d != null) {
+ // Logger logger = LoggerFactory.getILoggerFactory()
+ // .getLogger(loggerName);
+ // var slevel = toSlf4jLevel(level);
+ // return isEnabled(logger, slevel);
+ return d.isEnabled(loggerName, level);
+ }
+ return level.compareTo(System.Logger.Level.DEBUG) > 0;
+ }
+
+ public synchronized void log(
+ String loggerName,
+ java.lang.System.Logger.Level level,
+ String message,
+ @Nullable Throwable cause) {
+ LogRouter d = delegate;
+ if (d != null) {
+ d.log(loggerName, level, message, cause);
+ }
+ else {
+ events.add(LogEvent.of(level, loggerName, message, cause));
+ }
+
+ }
+
+ public synchronized void drain(
+ LogRouter delegate) {
+ this.delegate = Objects.requireNonNull(delegate);
+ LogEvent e;
+ while ((e = this.events.poll()) != null) {
+ delegate.log(e);
+ }
+ }
+
+ @Override
+ public void log(
+ LogEvent event) {
+ LogRouter d = delegate;
+ if (d != null) {
+ d.log(event);
+ }
+ else {
+ events.add(event);
+ }
+ }
+}
+
+
+
diff --git a/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java b/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java
new file mode 100644
index 00000000..5673a69e
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java
@@ -0,0 +1,51 @@
+package io.jstach.rainbowgum;
+
+import java.util.List;
+
+public interface RainbowGum {
+
+ public LogConfig config();
+
+ public LogRouter router();
+
+ public class Builder {
+ private LogConfig config = LogConfig.of(System::getProperty);
+ private LogRouter router;
+
+ private Builder() {
+ }
+
+ public Builder router(LogRouter router) {
+ this.router = router;
+ return this;
+ }
+
+ public Builder config(LogConfig config) {
+ this.config = config;
+ return this;
+ }
+
+ public RainbowGum build() {
+ var router = this.router;
+ var config = this.config;
+ if (router == null) {
+ router = LogRouter.of(List.of(LogAppender.of(config.defaultOutput(), null)));
+ }
+ return new RainbowGum() {
+
+ @Override
+ public LogRouter router() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public LogConfig config() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ };
+ }
+ }
+}
diff --git a/core/src/main/java/io/jstach/rainbowgum/jansi/JansiLogFormatter.java b/core/src/main/java/io/jstach/rainbowgum/jansi/JansiLogFormatter.java
new file mode 100644
index 00000000..e01fb608
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/jansi/JansiLogFormatter.java
@@ -0,0 +1,198 @@
+package io.jstach.rainbowgum.jansi;
+
+import java.lang.System.Logger.Level;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.fusesource.jansi.Ansi;
+import org.fusesource.jansi.Ansi.Attribute;
+import org.fusesource.jansi.Ansi.Color;
+
+import io.jstach.rainbowgum.LogEvent;
+import io.jstach.rainbowgum.LogFormatter;
+import io.jstach.rainbowgum.LogOutput;
+
+public class JansiLogFormatter implements LogFormatter {
+
+ private final LevelFormatter levelFormatter;
+ private final InstantFormatter instantFormatter;
+ private final ThrowableFormatter throwableFormatter;
+ private final boolean showThreadName;
+ private final boolean levelInBrackets;
+ private final boolean showShortLogName;
+ private final boolean showLogName;
+ private final List mdcKeys;
+
+ public JansiLogFormatter(
+ LevelFormatter levelFormatter,
+ InstantFormatter instantFormatter,
+ ThrowableFormatter throwableFormatter,
+ boolean showThreadName,
+ boolean levelInBrackets,
+ boolean showShortLogName,
+ boolean showLogName,
+ List mdcKeys) {
+ super();
+ this.levelFormatter = levelFormatter;
+ this.instantFormatter = instantFormatter;
+ this.throwableFormatter = throwableFormatter;
+ this.showThreadName = showThreadName;
+ this.levelInBrackets = levelInBrackets;
+ this.showShortLogName = showShortLogName;
+ this.showLogName = showLogName;
+ this.mdcKeys = mdcKeys;
+ }
+
+ public void format(
+ LogOutput output,
+ LogEvent logEvent) {
+
+ var level = logEvent.level();
+ var instant = logEvent.timeStamp();
+ var shortLogName = logEvent.loggerShortName();
+ var name = logEvent.loggerName();
+ @Nullable
+ Throwable t = logEvent.throwable();
+ var formattedMessage = logEvent.formattedMessage();
+
+ // StringBuilder buf = new StringBuilder(32);
+ StringBuilder b = new StringBuilder(32);
+ Ansi buf = Ansi.ansi(b);
+
+ // Append date-time if so configured
+
+ buf.fg(Color.CYAN);
+ buf.a(getFormattedDate(instant));
+ buf.fg(Color.DEFAULT);
+ buf.a(' ');
+
+ // Append current thread name if so configured
+ if (showThreadName) {
+ buf.a(Attribute.INTENSITY_FAINT);
+ buf.a("[");
+ buf.append(padRight(Thread.currentThread().getName(), 12)).append("]");
+ buf.a(Attribute.RESET);
+ buf.a(" ");
+ // buf.append('[');
+ // buf.append(
+ //
+ // buf.append("] ");
+ }
+
+ if (levelInBrackets)
+ buf.append('[');
+
+ // Append a readable representation of the log level
+
+ colorLevel(buf, level);
+
+ if (levelInBrackets)
+ buf.append(']');
+ buf.append(' ');
+
+ // Append the name of the log instance if so configured
+ if (showShortLogName) {
+ buf.append(String.valueOf(shortLogName));
+ } else if (showLogName) {
+ buf.fg(Color.MAGENTA);
+ buf.a(String.valueOf(name));
+ }
+
+ Map m = logEvent.getKeyValues();
+ List keys = mdcKeys;
+
+ if (!keys.isEmpty()) {
+ Collection<@NonNull String> ks;
+ if (keys.size() == 1 && "*".equals(keys.get(0))) {
+ ks = m.keySet();
+ } else {
+ ks = keys;
+ }
+ buf.fg(Color.WHITE);
+ buf.append(" ");
+ buf.a(Attribute.INTENSITY_FAINT);
+ buf.a("{");
+ boolean first = true;
+ for (String k : ks) {
+ String v = m.get(k);
+ if (v == null) {
+ continue;
+ }
+ if (first) {
+ first = false;
+ } else {
+ buf.append("&");
+ }
+ buf.append(URLEncoder.encode(k, StandardCharsets.US_ASCII));
+ buf.append("=");
+ buf.append(URLEncoder.encode(v, StandardCharsets.US_ASCII));
+
+ }
+ buf.a("}");
+ buf.fg(Color.DEFAULT);
+ buf.a(Attribute.RESET);
+ }
+
+ buf.fg(Color.DEFAULT);
+ buf.a(" - ");
+
+ buf.append(formattedMessage);
+
+ write(output, buf.toString(), t);
+ }
+
+ public static String padRight(
+ String s,
+ int n) {
+ return String.format("%-" + n + "s", s.substring(0, Math.min(s.length(), n)));
+ }
+
+ private Ansi colorLevel(
+ Ansi ansi,
+ Level level) {
+ String levelStr = levelFormatter.format(level);
+ switch (level) {
+ case ERROR:
+ ansi.a(Attribute.INTENSITY_BOLD).fg(Color.RED);
+ break;
+ case INFO:
+ ansi.a(Attribute.INTENSITY_BOLD).fg(Color.BLUE);
+ break;
+ case WARNING:
+ ansi.fg(Color.RED);
+ break;
+ case DEBUG:
+ ansi.a(Attribute.INTENSITY_FAINT).fg(Color.CYAN);
+ case TRACE:
+ default:
+ ansi.fg(Color.DEFAULT);
+ break;
+ }
+ return ansi.a(levelStr).fg(Color.DEFAULT).a(Attribute.RESET).a("");
+ }
+
+ private String getFormattedDate(
+ Instant instant) {
+ String dateText = instantFormatter.format(instant);
+ return dateText;
+ }
+
+ void write(
+ LogOutput output,
+ String buf,
+ @Nullable Throwable t) {
+ output.append(buf);
+ output.append("\n");
+ if (t != null) {
+ throwableFormatter.format(output, t);
+ }
+
+ }
+
+}
diff --git a/core/src/main/java/io/jstach/rainbowgum/jansi/package-info.java b/core/src/main/java/io/jstach/rainbowgum/jansi/package-info.java
new file mode 100644
index 00000000..8fff336b
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/jansi/package-info.java
@@ -0,0 +1,2 @@
+@org.eclipse.jdt.annotation.NonNullByDefault
+package io.jstach.rainbowgum.jansi;
\ No newline at end of file
diff --git a/core/src/main/java/io/jstach/rainbowgum/json/Grisu3.java b/core/src/main/java/io/jstach/rainbowgum/json/Grisu3.java
new file mode 100644
index 00000000..7b071667
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/json/Grisu3.java
@@ -0,0 +1,1056 @@
+package io.jstach.rainbowgum.json;
+
+//Copyright 2010 the V8 project authors. All rights reserved.
+//Redistribution and use in source and binary forms, with or without
+//modification, are permitted provided that the following conditions are
+//met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following
+// disclaimer in the documentation and/or other materials provided
+// with the distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived
+// from this software without specific prior written permission.
+//
+//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+//"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+//LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+//A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+//OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+//SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+//LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+//DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+//THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+//OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+//Ported to Java from Mozilla's version of V8-dtoa by Hannes Wallnoefer.
+//The original revision was 67d1049b0bf9 from the mozilla-central tree.
+
+//Modified by Rikard Pavelic do avoid allocations
+//and unused code paths due to external checks
+
+abstract class Grisu3 {
+
+ // FastDtoa will produce at most kFastDtoaMaximalLength digits.
+ static final int kFastDtoaMaximalLength = 17;
+
+ // The minimal and maximal target exponent define the range of w's binary
+ // exponent, where 'w' is the result of multiplying the input by a cached
+ // power
+ // of ten.
+ //
+ // A different range might be chosen on a different platform, to optimize
+ // digit
+ // generation, but a smaller range requires more powers of ten to be cached.
+ static final int minimal_target_exponent = -60;
+
+ private static final class DiyFp {
+
+ long f;
+ int e;
+
+ static final int kSignificandSize = 64;
+ static final long kUint64MSB = 0x8000000000000000L;
+ private static final long kM32 = 0xFFFFFFFFL;
+ private static final long k10MSBits = 0xFFC00000L << 32;
+
+ DiyFp() {
+ this.f = 0;
+ this.e = 0;
+ }
+
+ // this = this - other.
+ // The exponents of both numbers must be the same and the significand of
+ // this
+ // must be bigger than the significand of other.
+ // The result will not be normalized.
+ void subtract(
+ DiyFp other) {
+ f -= other.f;
+ }
+
+ // this = this * other.
+ void multiply(
+ DiyFp other) {
+ // Simply "emulates" a 128 bit multiplication.
+ // However: the resulting number only contains 64 bits. The least
+ // significant 64 bits are only used for rounding the most
+ // significant 64
+ // bits.
+ long a = f >>> 32;
+ long b = f & kM32;
+ long c = other.f >>> 32;
+ long d = other.f & kM32;
+ long ac = a * c;
+ long bc = b * c;
+ long ad = a * d;
+ long bd = b * d;
+ long tmp = (bd >>> 32) + (ad & kM32) + (bc & kM32);
+ // By adding 1U << 31 to tmp we round the final result.
+ // Halfway cases will be round up.
+ tmp += 1L << 31;
+ long result_f = ac + (ad >>> 32) + (bc >>> 32) + (tmp >>> 32);
+ e += other.e + 64;
+ f = result_f;
+ }
+
+ void normalize() {
+ long f = this.f;
+ int e = this.e;
+
+ // This method is mainly called for normalizing boundaries. In
+ // general
+ // boundaries need to be shifted by 10 bits. We thus optimize for
+ // this case.
+ while ((f & k10MSBits) == 0) {
+ f <<= 10;
+ e -= 10;
+ }
+ while ((f & kUint64MSB) == 0) {
+ f <<= 1;
+ e--;
+ }
+ this.f = f;
+ this.e = e;
+ }
+
+ void reset() {
+ e = 0;
+ f = 0;
+ }
+
+ @Override
+ public String toString() {
+ return "[DiyFp f:" + f + ", e:" + e + "]";
+ }
+
+ }
+
+ private static class CachedPowers {
+
+ static final double kD_1_LOG2_10 = 0.30102999566398114; // 1 / lg(10)
+
+ static class CachedPower {
+ final long significand;
+ final short binaryExponent;
+ final short decimalExponent;
+
+ CachedPower(
+ long significand,
+ short binaryExponent,
+ short decimalExponent) {
+ this.significand = significand;
+ this.binaryExponent = binaryExponent;
+ this.decimalExponent = decimalExponent;
+ }
+ }
+
+ static int getCachedPower(
+ int e,
+ int alpha,
+ DiyFp c_mk) {
+ final int kQ = DiyFp.kSignificandSize;
+ final double k = Math.ceil((alpha - e + kQ - 1) * kD_1_LOG2_10);
+ final int index = (GRISU_CACHE_OFFSET + (int) k - 1) / CACHED_POWERS_SPACING + 1;
+ final CachedPower cachedPower = CACHED_POWERS[index];
+
+ c_mk.f = cachedPower.significand;
+ c_mk.e = cachedPower.binaryExponent;
+ return cachedPower.decimalExponent;
+ }
+
+ // Code below is converted from GRISU_CACHE_NAME(8) in file
+ // "powers-ten.h"
+ // Regexp to convert this from original C++ source:
+ // \{GRISU_UINT64_C\((\w+), (\w+)\), (\-?\d+), (\-?\d+)\}
+
+ // interval between entries of the powers cache below
+ static final int CACHED_POWERS_SPACING = 8;
+
+ static final CachedPower[] CACHED_POWERS = {
+ new CachedPower(0xe61acf033d1a45dfL, (short) -1087, (short) -308),
+ new CachedPower(0xab70fe17c79ac6caL, (short) -1060, (short) -300),
+ new CachedPower(0xff77b1fcbebcdc4fL, (short) -1034, (short) -292),
+ new CachedPower(0xbe5691ef416bd60cL, (short) -1007, (short) -284),
+ new CachedPower(0x8dd01fad907ffc3cL, (short) -980, (short) -276),
+ new CachedPower(0xd3515c2831559a83L, (short) -954, (short) -268),
+ new CachedPower(0x9d71ac8fada6c9b5L, (short) -927, (short) -260),
+ new CachedPower(0xea9c227723ee8bcbL, (short) -901, (short) -252),
+ new CachedPower(0xaecc49914078536dL, (short) -874, (short) -244),
+ new CachedPower(0x823c12795db6ce57L, (short) -847, (short) -236),
+ new CachedPower(0xc21094364dfb5637L, (short) -821, (short) -228),
+ new CachedPower(0x9096ea6f3848984fL, (short) -794, (short) -220),
+ new CachedPower(0xd77485cb25823ac7L, (short) -768, (short) -212),
+ new CachedPower(0xa086cfcd97bf97f4L, (short) -741, (short) -204),
+ new CachedPower(0xef340a98172aace5L, (short) -715, (short) -196),
+ new CachedPower(0xb23867fb2a35b28eL, (short) -688, (short) -188),
+ new CachedPower(0x84c8d4dfd2c63f3bL, (short) -661, (short) -180),
+ new CachedPower(0xc5dd44271ad3cdbaL, (short) -635, (short) -172),
+ new CachedPower(0x936b9fcebb25c996L, (short) -608, (short) -164),
+ new CachedPower(0xdbac6c247d62a584L, (short) -582, (short) -156),
+ new CachedPower(0xa3ab66580d5fdaf6L, (short) -555, (short) -148),
+ new CachedPower(0xf3e2f893dec3f126L, (short) -529, (short) -140),
+ new CachedPower(0xb5b5ada8aaff80b8L, (short) -502, (short) -132),
+ new CachedPower(0x87625f056c7c4a8bL, (short) -475, (short) -124),
+ new CachedPower(0xc9bcff6034c13053L, (short) -449, (short) -116),
+ new CachedPower(0x964e858c91ba2655L, (short) -422, (short) -108),
+ new CachedPower(0xdff9772470297ebdL, (short) -396, (short) -100),
+ new CachedPower(0xa6dfbd9fb8e5b88fL, (short) -369, (short) -92),
+ new CachedPower(0xf8a95fcf88747d94L, (short) -343, (short) -84),
+ new CachedPower(0xb94470938fa89bcfL, (short) -316, (short) -76),
+ new CachedPower(0x8a08f0f8bf0f156bL, (short) -289, (short) -68),
+ new CachedPower(0xcdb02555653131b6L, (short) -263, (short) -60),
+ new CachedPower(0x993fe2c6d07b7facL, (short) -236, (short) -52),
+ new CachedPower(0xe45c10c42a2b3b06L, (short) -210, (short) -44),
+ new CachedPower(0xaa242499697392d3L, (short) -183, (short) -36),
+ new CachedPower(0xfd87b5f28300ca0eL, (short) -157, (short) -28),
+ new CachedPower(0xbce5086492111aebL, (short) -130, (short) -20),
+ new CachedPower(0x8cbccc096f5088ccL, (short) -103, (short) -12),
+ new CachedPower(0xd1b71758e219652cL, (short) -77, (short) -4),
+ new CachedPower(0x9c40000000000000L, (short) -50, (short) 4),
+ new CachedPower(0xe8d4a51000000000L, (short) -24, (short) 12),
+ new CachedPower(0xad78ebc5ac620000L, (short) 3, (short) 20),
+ new CachedPower(0x813f3978f8940984L, (short) 30, (short) 28),
+ new CachedPower(0xc097ce7bc90715b3L, (short) 56, (short) 36),
+ new CachedPower(0x8f7e32ce7bea5c70L, (short) 83, (short) 44),
+ new CachedPower(0xd5d238a4abe98068L, (short) 109, (short) 52),
+ new CachedPower(0x9f4f2726179a2245L, (short) 136, (short) 60),
+ new CachedPower(0xed63a231d4c4fb27L, (short) 162, (short) 68),
+ new CachedPower(0xb0de65388cc8ada8L, (short) 189, (short) 76),
+ new CachedPower(0x83c7088e1aab65dbL, (short) 216, (short) 84),
+ new CachedPower(0xc45d1df942711d9aL, (short) 242, (short) 92),
+ new CachedPower(0x924d692ca61be758L, (short) 269, (short) 100),
+ new CachedPower(0xda01ee641a708deaL, (short) 295, (short) 108),
+ new CachedPower(0xa26da3999aef774aL, (short) 322, (short) 116),
+ new CachedPower(0xf209787bb47d6b85L, (short) 348, (short) 124),
+ new CachedPower(0xb454e4a179dd1877L, (short) 375, (short) 132),
+ new CachedPower(0x865b86925b9bc5c2L, (short) 402, (short) 140),
+ new CachedPower(0xc83553c5c8965d3dL, (short) 428, (short) 148),
+ new CachedPower(0x952ab45cfa97a0b3L, (short) 455, (short) 156),
+ new CachedPower(0xde469fbd99a05fe3L, (short) 481, (short) 164),
+ new CachedPower(0xa59bc234db398c25L, (short) 508, (short) 172),
+ new CachedPower(0xf6c69a72a3989f5cL, (short) 534, (short) 180),
+ new CachedPower(0xb7dcbf5354e9beceL, (short) 561, (short) 188),
+ new CachedPower(0x88fcf317f22241e2L, (short) 588, (short) 196),
+ new CachedPower(0xcc20ce9bd35c78a5L, (short) 614, (short) 204),
+ new CachedPower(0x98165af37b2153dfL, (short) 641, (short) 212),
+ new CachedPower(0xe2a0b5dc971f303aL, (short) 667, (short) 220),
+ new CachedPower(0xa8d9d1535ce3b396L, (short) 694, (short) 228),
+ new CachedPower(0xfb9b7cd9a4a7443cL, (short) 720, (short) 236),
+ new CachedPower(0xbb764c4ca7a44410L, (short) 747, (short) 244),
+ new CachedPower(0x8bab8eefb6409c1aL, (short) 774, (short) 252),
+ new CachedPower(0xd01fef10a657842cL, (short) 800, (short) 260),
+ new CachedPower(0x9b10a4e5e9913129L, (short) 827, (short) 268),
+ new CachedPower(0xe7109bfba19c0c9dL, (short) 853, (short) 276),
+ new CachedPower(0xac2820d9623bf429L, (short) 880, (short) 284),
+ new CachedPower(0x80444b5e7aa7cf85L, (short) 907, (short) 292),
+ new CachedPower(0xbf21e44003acdd2dL, (short) 933, (short) 300),
+ new CachedPower(0x8e679c2f5e44ff8fL, (short) 960, (short) 308),
+ new CachedPower(0xd433179d9c8cb841L, (short) 986, (short) 316),
+ new CachedPower(0x9e19db92b4e31ba9L, (short) 1013, (short) 324),
+ new CachedPower(0xeb96bf6ebadf77d9L, (short) 1039, (short) 332),
+ new CachedPower(0xaf87023b9bf0ee6bL, (short) 1066, (short) 340) };
+
+ // nb elements (8): 82
+
+ static final int GRISU_CACHE_OFFSET = 308;
+ }
+
+ private static class DoubleHelper {
+
+ static final long kExponentMask = 0x7FF0000000000000L;
+ static final long kSignificandMask = 0x000FFFFFFFFFFFFFL;
+ static final long kHiddenBit = 0x0010000000000000L;
+
+ static void asDiyFp(
+ long d64,
+ DiyFp v) {
+ v.f = significand(d64);
+ v.e = exponent(d64);
+ }
+
+ // this->Significand() must not be 0.
+ static void asNormalizedDiyFp(
+ long d64,
+ DiyFp w) {
+ long f = significand(d64);
+ int e = exponent(d64);
+
+ // The current double could be a denormal.
+ while ((f & kHiddenBit) == 0) {
+ f <<= 1;
+ e--;
+ }
+ // Do the final shifts in one go. Don't forget the hidden bit (the
+ // '-1').
+ f <<= DiyFp.kSignificandSize - kSignificandSize - 1;
+ e -= DiyFp.kSignificandSize - kSignificandSize - 1;
+ w.f = f;
+ w.e = e;
+ }
+
+ static int exponent(
+ long d64) {
+ if (isDenormal(d64))
+ return kDenormalExponent;
+
+ int biased_e = (int) (((d64 & kExponentMask) >>> kSignificandSize) & 0xffffffffL);
+ return biased_e - kExponentBias;
+ }
+
+ static long significand(
+ long d64) {
+ long significand = d64 & kSignificandMask;
+ if (!isDenormal(d64)) {
+ return significand + kHiddenBit;
+ }
+ else {
+ return significand;
+ }
+ }
+
+ // Returns true if the double is a denormal.
+ private static boolean isDenormal(
+ long d64) {
+ return (d64 & kExponentMask) == 0L;
+ }
+
+ // Returns the two boundaries of first argument.
+ // The bigger boundary (m_plus) is normalized. The lower boundary has
+ // the same
+ // exponent as m_plus.
+ static void normalizedBoundaries(
+ DiyFp v,
+ long d64,
+ DiyFp m_minus,
+ DiyFp m_plus) {
+ asDiyFp(d64, v);
+ final boolean significand_is_zero = (v.f == kHiddenBit);
+ m_plus.f = (v.f << 1) + 1;
+ m_plus.e = v.e - 1;
+ m_plus.normalize();
+ if (significand_is_zero && v.e != kDenormalExponent) {
+ // The boundary is closer. Think of v = 1000e10 and v- = 9999e9.
+ // Then the boundary (== (v - v-)/2) is not just at a distance
+ // of 1e9 but
+ // at a distance of 1e8.
+ // The only exception is for the smallest normal: the largest
+ // denormal is
+ // at the same distance as its successor.
+ // Note: denormals have the same exponent as the smallest
+ // normals.
+ m_minus.f = (v.f << 2) - 1;
+ m_minus.e = v.e - 2;
+ }
+ else {
+ m_minus.f = (v.f << 1) - 1;
+ m_minus.e = v.e - 1;
+ }
+ m_minus.f = m_minus.f << (m_minus.e - m_plus.e);
+ m_minus.e = m_plus.e;
+ }
+
+ private static final int kSignificandSize = 52; // Excludes the hidden
+ // bit.
+ private static final int kExponentBias = 0x3FF + kSignificandSize;
+ private static final int kDenormalExponent = -kExponentBias + 1;
+
+ }
+
+ static class FastDtoa {
+
+ // Adjusts the last digit of the generated number, and screens out
+ // generated
+ // solutions that may be inaccurate. A solution may be inaccurate if it
+ // is
+ // outside the safe interval, or if we ctannot prove that it is closer
+ // to the
+ // input than a neighboring representation of the same length.
+ //
+ // Input: * buffer containing the digits of too_high / 10^kappa
+ // * distance_too_high_w == (too_high - w).f() * unit
+ // * unsafe_interval == (too_high - too_low).f() * unit
+ // * rest = (too_high - buffer * 10^kappa).f() * unit
+ // * ten_kappa = 10^kappa * unit
+ // * unit = the common multiplier
+ // Output: returns true if the buffer is guaranteed to contain the
+ // closest
+ // representable number to the input.
+ // Modifies the generated digits in the buffer to approach (round
+ // towards) w.
+ static boolean roundWeed(
+ final FastDtoaBuilder buffer,
+ final long distance_too_high_w,
+ final long unsafe_interval,
+ long rest,
+ final long ten_kappa,
+ final long unit) {
+ final long small_distance = distance_too_high_w - unit;
+ final long big_distance = distance_too_high_w + unit;
+ // Let w_low = too_high - big_distance, and
+ // w_high = too_high - small_distance.
+ // Note: w_low < w < w_high
+ //
+ // The real w (* unit) must lie somewhere inside the interval
+ // ]w_low; w_low[ (often written as "(w_low; w_low)")
+
+ // Basically the buffer currently contains a number in the unsafe
+ // interval
+ // ]too_low; too_high[ with too_low < w < too_high
+ //
+ // too_high - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ // - - - - -
+ // ^v 1 unit ^ ^ ^ ^
+ // boundary_high --------------------- . . . .
+ // ^v 1 unit . . . .
+ // - - - - - - - - - - - - - - - - - - - + - - + - - - - - - . .
+ // . . ^ . .
+ // . big_distance . . .
+ // . . . . rest
+ // small_distance . . . .
+ // v . . . .
+ // w_high - - - - - - - - - - - - - - - - - - . . . .
+ // ^v 1 unit . . . .
+ // w ---------------------------------------- . . . .
+ // ^v 1 unit v . . .
+ // w_low - - - - - - - - - - - - - - - - - - - - - . . .
+ // . . v
+ // buffer
+ // --------------------------------------------------+-------+--------
+ // . .
+ // safe_interval .
+ // v .
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .
+ // ^v 1 unit .
+ // boundary_low ------------------------- unsafe_interval
+ // ^v 1 unit v
+ // too_low - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ // - - - -
+ //
+ //
+ // Note that the value of buffer could lie anywhere inside the range
+ // too_low
+ // to too_high.
+ //
+ // boundary_low, boundary_high and w are approximations of the real
+ // boundaries
+ // and v (the input number). They are guaranteed to be precise up to
+ // one unit.
+ // In fact the error is guaranteed to be strictly less than one
+ // unit.
+ //
+ // Anything that lies outside the unsafe interval is guaranteed not
+ // to round
+ // to v when read again.
+ // Anything that lies inside the safe interval is guaranteed to
+ // round to v
+ // when read again.
+ // If the number inside the buffer lies inside the unsafe interval
+ // but not
+ // inside the safe interval then we simply do not know and bail out
+ // (returning
+ // false).
+ //
+ // Similarly we have to take into account the imprecision of 'w'
+ // when rounding
+ // the buffer. If we have two potential representations we need to
+ // make sure
+ // that the chosen one is closer to w_low and w_high since v can be
+ // anywhere
+ // between them.
+ //
+ // By generating the digits of too_high we got the largest (closest
+ // to
+ // too_high) buffer that is still in the unsafe interval. In the
+ // case where
+ // w_high < buffer < too_high we try to decrement the buffer.
+ // This way the buffer approaches (rounds towards) w.
+ // There are 3 conditions that stop the decrementation process:
+ // 1) the buffer is already below w_high
+ // 2) decrementing the buffer would make it leave the unsafe
+ // interval
+ // 3) decrementing the buffer would yield a number below w_high and
+ // farther
+ // away than the current number. In other words:
+ // (buffer{-1} < w_high) && w_high - buffer{-1} > buffer - w_high
+ // Instead of using the buffer directly we use its distance to
+ // too_high.
+ // Conceptually rest ~= too_high - buffer
+ while (rest < small_distance && // Negated condition 1
+ unsafe_interval - rest >= ten_kappa && // Negated condition
+ // 2
+ (rest + ten_kappa < small_distance || // buffer{-1} > w_high
+ small_distance - rest >= rest + ten_kappa - small_distance)) {
+ buffer.decreaseLast();
+ rest += ten_kappa;
+ }
+
+ // We have approached w+ as much as possible. We now test if
+ // approaching w-
+ // would require changing the buffer. If yes, then we have two
+ // possible
+ // representations close to w, but we cannot decide which one is
+ // closer.
+ if (rest < big_distance
+ && unsafe_interval - rest >= ten_kappa
+ && (rest + ten_kappa < big_distance || big_distance - rest > rest + ten_kappa - big_distance)) {
+ return false;
+ }
+
+ // Weeding test.
+ // The safe interval is [too_low + 2 ulp; too_high - 2 ulp]
+ // Since too_low = too_high - unsafe_interval this is equivalent to
+ // [too_high - unsafe_interval + 4 ulp; too_high - 2 ulp]
+ // Conceptually we have: rest ~= too_high - buffer
+ return (2 * unit <= rest) && (rest <= unsafe_interval - 4 * unit);
+ }
+
+ static final int kTen4 = 10000;
+ static final int kTen5 = 100000;
+ static final int kTen6 = 1000000;
+ static final int kTen7 = 10000000;
+ static final int kTen8 = 100000000;
+ static final int kTen9 = 1000000000;
+
+ // Returns the biggest power of ten that is less than or equal than the
+ // given
+ // number. We furthermore receive the maximum number of bits 'number'
+ // has.
+ // If number_bits == 0 then 0^-1 is returned
+ // The number of bits must be <= 32.
+ // Precondition: (1 << number_bits) <= number < (1 << (number_bits +
+ // 1)).
+ static long biggestPowerTen(
+ int number,
+ int number_bits) {
+ int power, exponent;
+ switch (number_bits) {
+ case 32:
+ case 31:
+ case 30:
+ if (kTen9 <= number) {
+ power = kTen9;
+ exponent = 9;
+ break;
+ } // else fallthrough
+ case 29:
+ case 28:
+ case 27:
+ if (kTen8 <= number) {
+ power = kTen8;
+ exponent = 8;
+ break;
+ } // else fallthrough
+ case 26:
+ case 25:
+ case 24:
+ if (kTen7 <= number) {
+ power = kTen7;
+ exponent = 7;
+ break;
+ } // else fallthrough
+ case 23:
+ case 22:
+ case 21:
+ case 20:
+ if (kTen6 <= number) {
+ power = kTen6;
+ exponent = 6;
+ break;
+ } // else fallthrough
+ case 19:
+ case 18:
+ case 17:
+ if (kTen5 <= number) {
+ power = kTen5;
+ exponent = 5;
+ break;
+ } // else fallthrough
+ case 16:
+ case 15:
+ case 14:
+ if (kTen4 <= number) {
+ power = kTen4;
+ exponent = 4;
+ break;
+ } // else fallthrough
+ case 13:
+ case 12:
+ case 11:
+ case 10:
+ if (1000 <= number) {
+ power = 1000;
+ exponent = 3;
+ break;
+ } // else fallthrough
+ case 9:
+ case 8:
+ case 7:
+ if (100 <= number) {
+ power = 100;
+ exponent = 2;
+ break;
+ } // else fallthrough
+ case 6:
+ case 5:
+ case 4:
+ if (10 <= number) {
+ power = 10;
+ exponent = 1;
+ break;
+ } // else fallthrough
+ case 3:
+ case 2:
+ case 1:
+ if (1 <= number) {
+ power = 1;
+ exponent = 0;
+ break;
+ } // else fallthrough
+ case 0:
+ power = 0;
+ exponent = -1;
+ break;
+ default:
+ // Following assignments are here to silence compiler
+ // warnings.
+ power = 0;
+ exponent = 0;
+ // UNREACHABLE();
+ }
+ return ((long) power << 32) | (0xffffffffL & exponent);
+ }
+
+ // Generates the digits of input number w.
+ // w is a floating-point number (DiyFp), consisting of a significand and
+ // an
+ // exponent. Its exponent is bounded by minimal_target_exponent and
+ // maximal_target_exponent.
+ // Hence -60 <= w.e() <= -32.
+ //
+ // Returns false if it fails, in which case the generated digits in the
+ // buffer
+ // should not be used.
+ // Preconditions:
+ // * low, w and high are correct up to 1 ulp (unit in the last place).
+ // That
+ // is, their error must be less that a unit of their last digits.
+ // * low.e() == w.e() == high.e()
+ // * low < w < high, and taking into account their error: low~ <= high~
+ // * minimal_target_exponent <= w.e() <= maximal_target_exponent
+ // Postconditions: returns false if procedure fails.
+ // otherwise:
+ // * buffer is not null-terminated, but len contains the number of
+ // digits.
+ // * buffer contains the shortest possible decimal digit-sequence
+ // such that LOW < buffer * 10^kappa < HIGH, where LOW and HIGH are the
+ // correct values of low and high (without their error).
+ // * if more than one decimal representation gives the minimal number of
+ // decimal digits then the one closest to W (where W is the correct
+ // value
+ // of w) is chosen.
+ // Remark: this procedure takes into account the imprecision of its
+ // input
+ // numbers. If the precision is not enough to guarantee all the
+ // postconditions
+ // then false is returned. This usually happens rarely (~0.5%).
+ //
+ // Say, for the sake of example, that
+ // w.e() == -48, and w.f() == 0x1234567890abcdef
+ // w's value can be computed by w.f() * 2^w.e()
+ // We can obtain w's integral digits by simply shifting w.f() by -w.e().
+ // -> w's integral part is 0x1234
+ // w's fractional part is therefore 0x567890abcdef.
+ // Printing w's integral part is easy (simply print 0x1234 in decimal).
+ // In order to print its fraction we repeatedly multiply the fraction by
+ // 10 and
+ // get each digit. Example the first digit after the point would be
+ // computed by
+ // (0x567890abcdef * 10) >> 48. -> 3
+ // The whole thing becomes slightly more complicated because we want to
+ // stop
+ // once we have enough digits. That is, once the digits inside the
+ // buffer
+ // represent 'w' we can stop. Everything inside the interval low - high
+ // represents w. However we have to pay attention to low, high and w's
+ // imprecision.
+ static boolean digitGen(
+ FastDtoaBuilder buffer,
+ int mk) {
+ final DiyFp low = buffer.scaled_boundary_minus;
+ final DiyFp w = buffer.scaled_w;
+ final DiyFp high = buffer.scaled_boundary_plus;
+
+ // low, w and high are imprecise, but by less than one ulp (unit in
+ // the last
+ // place).
+ // If we remove (resp. add) 1 ulp from low (resp. high) we are
+ // certain that
+ // the new numbers are outside of the interval we want the final
+ // representation to lie in.
+ // Inversely adding (resp. removing) 1 ulp from low (resp. high)
+ // would yield
+ // numbers that are certain to lie in the interval. We will use this
+ // fact
+ // later on.
+ // We will now start by generating the digits within the uncertain
+ // interval. Later we will weed out representations that lie outside
+ // the safe
+ // interval and thus _might_ lie outside the correct interval.
+ long unit = 1;
+ final DiyFp too_low = buffer.too_low;
+ too_low.f = low.f - unit;
+ too_low.e = low.e;
+ final DiyFp too_high = buffer.too_high;
+ too_high.f = high.f + unit;
+ too_high.e = high.e;
+ // too_low and too_high are guaranteed to lie outside the interval
+ // we want the
+ // generated number in.
+ final DiyFp unsafe_interval = buffer.unsafe_interval;
+ unsafe_interval.f = too_high.f;
+ unsafe_interval.e = too_high.e;
+ unsafe_interval.subtract(too_low);
+ // We now cut the input number into two parts: the integral digits
+ // and the
+ // fractionals. We will not write any decimal separator though, but
+ // adapt
+ // kappa instead.
+ // Reminder: we are currently computing the digits (stored inside
+ // the buffer)
+ // such that: too_low < buffer * 10^kappa < too_high
+ // We use too_high for the digit_generation and stop as soon as
+ // possible.
+ // If we stop early we effectively round down.
+ final DiyFp one = buffer.one;
+ one.f = 1L << -w.e;
+ one.e = w.e;
+ // Division by one is a shift.
+ int integrals = (int) ((too_high.f >>> -one.e) & 0xffffffffL);
+ // Modulo by one is an and.
+ long fractionals = too_high.f & (one.f - 1);
+ long result = biggestPowerTen(integrals, DiyFp.kSignificandSize - (-one.e));
+ int divider = (int) ((result >>> 32) & 0xffffffffL);
+ int divider_exponent = (int) (result & 0xffffffffL);
+ int kappa = divider_exponent + 1;
+ // Loop invariant: buffer = too_high / 10^kappa (integer division)
+ // The invariant holds for the first iteration: kappa has been
+ // initialized
+ // with the divider exponent + 1. And the divider is the biggest
+ // power of ten
+ // that is smaller than integrals.
+ while (kappa > 0) {
+ int digit = integrals / divider;
+ buffer.append((byte) ('0' + digit));
+ integrals %= divider;
+ kappa--;
+ // Note that kappa now equals the exponent of the divider and
+ // that the
+ // invariant thus holds again.
+ final long rest = ((long) integrals << -one.e) + fractionals;
+ // Invariant: too_high = buffer * 10^kappa + DiyFp(rest,
+ // one.e())
+ // Reminder: unsafe_interval.e() == one.e()
+ if (rest < unsafe_interval.f) {
+ // Rounding down (by not emitting the remaining digits)
+ // yields a number
+ // that lies within the unsafe interval.
+ buffer.point = buffer.end - mk + kappa;
+ final DiyFp minus_round = buffer.minus_round;
+ minus_round.f = too_high.f;
+ minus_round.e = too_high.e;
+ minus_round.subtract(w);
+ return roundWeed(buffer, minus_round.f, unsafe_interval.f, rest, (long) divider << -one.e, unit);
+ }
+ divider /= 10;
+ }
+
+ // The integrals have been generated. We are at the point of the
+ // decimal
+ // separator. In the following loop we simply multiply the remaining
+ // digits by
+ // 10 and divide by one. We just need to pay attention to multiply
+ // associated
+ // data (like the interval or 'unit'), too.
+ // Instead of multiplying by 10 we multiply by 5 (cheaper operation)
+ // and
+ // increase its (imaginary) exponent. At the same time we decrease
+ // the
+ // divider's (one's) exponent and shift its significand.
+ // Basically, if fractionals was a DiyFp (with fractionals.e ==
+ // one.e):
+ // fractionals.f *= 10;
+ // fractionals.f >>= 1; fractionals.e++; // value remains unchanged.
+ // one.f >>= 1; one.e++; // value remains unchanged.
+ // and we have again fractionals.e == one.e which allows us to
+ // divide
+ // fractionals.f() by one.f()
+ // We simply combine the *= 10 and the >>= 1.
+ while (true) {
+ fractionals *= 5;
+ unit *= 5;
+ unsafe_interval.f = unsafe_interval.f * 5;
+ unsafe_interval.e = unsafe_interval.e + 1; // Will be optimized
+ // out.
+ one.f = one.f >>> 1;
+ one.e = one.e + 1;
+ // Integer division by one.
+ final int digit = (int) ((fractionals >>> -one.e) & 0xffffffffL);
+ buffer.append((byte) ('0' + digit));
+ fractionals &= one.f - 1; // Modulo by one.
+ kappa--;
+ if (fractionals < unsafe_interval.f) {
+ buffer.point = buffer.end - mk + kappa;
+ final DiyFp minus_round = buffer.minus_round;
+ minus_round.f = too_high.f;
+ minus_round.e = too_high.e;
+ minus_round.subtract(w);
+ return roundWeed(buffer, minus_round.f * unit, unsafe_interval.f, fractionals, one.f, unit);
+ }
+ }
+ }
+ }
+
+ public static boolean tryConvert(
+ final double value,
+ final FastDtoaBuilder buffer) {
+ final long bits;
+ final int firstDigit;
+ buffer.reset();
+ if (value < 0) {
+ buffer.append((byte) '-');
+ bits = Double.doubleToLongBits(-value);
+ firstDigit = 1;
+ }
+ else {
+ bits = Double.doubleToLongBits(value);
+ firstDigit = 0;
+ }
+
+ // Provides a decimal representation of v.
+ // Returns true if it succeeds, otherwise the result cannot be trusted.
+ // There will be *length digits inside the buffer (not null-terminated).
+ // If the function returns true then
+ // v == (double) (buffer * 10^decimal_exponent).
+ // The digits in the buffer are the shortest representation possible: no
+ // 0.09999999999999999 instead of 0.1. The shorter representation will
+ // even be
+ // chosen even if the longer one would be closer to v.
+ // The last digit will be closest to the actual v. That is, even if
+ // several
+ // digits might correctly yield 'v' when read again, the closest will be
+ // computed.
+ final int mk = buffer.initialize(bits);
+
+ // DigitGen will generate the digits of scaled_w. Therefore we have
+ // v == (double) (scaled_w * 10^-mk).
+ // Set decimal_exponent == -mk and pass it to DigitGen. If scaled_w is
+ // not an
+ // integer than it will be updated. For instance if scaled_w == 1.23
+ // then
+ // the buffer will be filled with "123" und the decimal_exponent will be
+ // decreased by 2.
+ if (FastDtoa.digitGen(buffer, mk)) {
+ buffer.write(firstDigit);
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ static class FastDtoaBuilder {
+
+ private final DiyFp v = new DiyFp();
+ private final DiyFp w = new DiyFp();
+ private final DiyFp boundary_minus = new DiyFp();
+ private final DiyFp boundary_plus = new DiyFp();
+ private final DiyFp ten_mk = new DiyFp();
+ private final DiyFp scaled_w = new DiyFp();
+ private final DiyFp scaled_boundary_minus = new DiyFp();
+ private final DiyFp scaled_boundary_plus = new DiyFp();
+
+ private final DiyFp too_low = new DiyFp();
+ private final DiyFp too_high = new DiyFp();
+ private final DiyFp unsafe_interval = new DiyFp();
+ private final DiyFp one = new DiyFp();
+ private final DiyFp minus_round = new DiyFp();
+
+ int initialize(
+ final long bits) {
+ DoubleHelper.asNormalizedDiyFp(bits, w);
+ // boundary_minus and boundary_plus are the boundaries between v and
+ // its
+ // closest floating-point neighbors. Any number strictly between
+ // boundary_minus and boundary_plus will round to v when convert to
+ // a double.
+ // Grisu3 will never output representations that lie exactly on a
+ // boundary.
+ boundary_minus.reset();
+ boundary_plus.reset();
+ DoubleHelper.normalizedBoundaries(v, bits, boundary_minus, boundary_plus);
+ ten_mk.reset(); // Cached power of ten: 10^-k
+ final int mk = CachedPowers.getCachedPower(w.e + DiyFp.kSignificandSize, minimal_target_exponent, ten_mk);
+ // Note that ten_mk is only an approximation of 10^-k. A DiyFp only
+ // contains a
+ // 64 bit significand and ten_mk is thus only precise up to 64 bits.
+
+ // The DiyFp::Times procedure rounds its result, and ten_mk is
+ // approximated
+ // too. The variable scaled_w (as well as
+ // scaled_boundary_minus/plus) are now
+ // off by a small amount.
+ // In fact: scaled_w - w*10^k < 1ulp (unit in the last place) of
+ // scaled_w.
+ // In other words: let f = scaled_w.f() and e = scaled_w.e(), then
+ // (f-1) * 2^e < w*10^k < (f+1) * 2^e
+ scaled_w.f = w.f;
+ scaled_w.e = w.e;
+ scaled_w.multiply(ten_mk);
+ // In theory it would be possible to avoid some recomputations by
+ // computing
+ // the difference between w and boundary_minus/plus (a power of 2)
+ // and to
+ // compute scaled_boundary_minus/plus by subtracting/adding from
+ // scaled_w. However the code becomes much less readable and the
+ // speed
+ // enhancements are not terriffic.
+ scaled_boundary_minus.f = boundary_minus.f;
+ scaled_boundary_minus.e = boundary_minus.e;
+ scaled_boundary_minus.multiply(ten_mk);
+ scaled_boundary_plus.f = boundary_plus.f;
+ scaled_boundary_plus.e = boundary_plus.e;
+ scaled_boundary_plus.multiply(ten_mk);
+
+ return mk;
+ }
+
+ // allocate buffer for generated digits + extra notation + padding
+ // zeroes
+ private final byte[] chars = new byte[kFastDtoaMaximalLength + 10];
+ private int end = 0;
+ private int point;
+
+ void reset() {
+ end = 0;
+ }
+
+ void append(
+ byte c) {
+ chars[end++] = c;
+ }
+
+ void decreaseLast() {
+ chars[end - 1]--;
+ }
+
+ @Override
+ public String toString() {
+ return "[chars:" + new String(chars, 0, end) + ", point:" + point + "]";
+ }
+
+ int copyTo(
+ final byte[] target,
+ final int position) {
+ for (int i = 0; i < end; i++) {
+ target[i + position] = chars[i];
+ }
+ return end;
+ }
+
+ public void write(
+ int firstDigit) {
+ // check for minus sign
+ int decPoint = point - firstDigit;
+ if (decPoint < -5 || decPoint > 21) {
+ toExponentialFormat(firstDigit, decPoint);
+ }
+ else {
+ toFixedFormat(firstDigit, decPoint);
+ }
+ }
+
+ private void toFixedFormat(
+ int firstDigit,
+ int decPoint) {
+ if (point < end) {
+ // insert decimal point
+ if (decPoint > 0) {
+ // >= 1, split decimals and insert point
+ for (int i = end; i >= point; i--) {
+ chars[i + 1] = chars[i];
+ }
+ chars[point] = '.';
+ end++;
+ }
+ else {
+ // < 1,
+ final int offset = 2 - decPoint;
+ for (int i = end + firstDigit; i >= firstDigit; i--) {
+ chars[i + offset] = chars[i];
+ }
+ chars[firstDigit] = '0';
+ chars[firstDigit + 1] = '.';
+ if (decPoint < 0) {
+ int target = firstDigit + 2 - decPoint;
+ for (int i = firstDigit + 2; i < target; i++) {
+ chars[i] = '0';
+ }
+ }
+ end += 2 - decPoint;
+ }
+ }
+ else if (point > end) {
+ // large integer, add trailing zeroes
+ for (int i = end; i < point; i++) {
+ chars[i] = '0';
+ }
+ end += point - end;
+ chars[end] = '.';
+ chars[end + 1] = '0';
+ end += 2;
+ }
+ else {
+ chars[end] = '.';
+ chars[end + 1] = '0';
+ end += 2;
+ }
+ }
+
+ private void toExponentialFormat(
+ int firstDigit,
+ int decPoint) {
+ if (end - firstDigit > 1) {
+ // insert decimal point if more than one digit was produced
+ int dot = firstDigit + 1;
+ System.arraycopy(chars, dot, chars, dot + 1, end - dot);
+ chars[dot] = '.';
+ end++;
+ }
+ chars[end++] = 'E';
+ byte sign = '+';
+ int exp = decPoint - 1;
+ if (exp < 0) {
+ sign = '-';
+ exp = -exp;
+ }
+ chars[end++] = sign;
+
+ int charPos = exp > 99 ? end + 2 : exp > 9 ? end + 1 : end;
+ end = charPos + 1;
+
+ do {
+ int r = exp % 10;
+ chars[charPos--] = digits[r];
+ exp = exp / 10;
+ }
+ while (exp != 0);
+ }
+
+ final static byte[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
+ }
+}
diff --git a/core/src/main/java/io/jstach/rainbowgum/json/JsonLogAppender.java b/core/src/main/java/io/jstach/rainbowgum/json/JsonLogAppender.java
new file mode 100644
index 00000000..85877f48
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/json/JsonLogAppender.java
@@ -0,0 +1,244 @@
+package io.jstach.rainbowgum.json;
+
+import static io.jstach.rainbowgum.json.RawJsonWriter.COMMA;
+import static io.jstach.rainbowgum.json.RawJsonWriter.OBJECT_END;
+import static io.jstach.rainbowgum.json.RawJsonWriter.OBJECT_START;
+import static io.jstach.rainbowgum.json.RawJsonWriter.SEMI;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.System.Logger.Level;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import io.jstach.rainbowgum.LogAppender;
+import io.jstach.rainbowgum.LogEncoder;
+import io.jstach.rainbowgum.LogEvent;
+import io.jstach.rainbowgum.LogFormatter.LevelFormatter;
+import io.jstach.rainbowgum.LogConfig;
+
+public class JsonLogAppender implements LogAppender {
+
+ /*
+ * { "version": "1.1", "host": "example.org", "short_message":
+ * "A short message that helps you identify what is going on",
+ * "full_message": "Backtrace here\n\nmore stuff", "timestamp":
+ * 1385053862.3072, "level": 1, "_user_id": 9001, "_some_info": "foo",
+ * "_some_env_var": "bar" }
+ */
+
+ private final String host;
+ private final Map headers;
+ private final RawJsonWriter raw = new RawJsonWriter(1024 * 8);
+ private final LogEncoder out;
+ private final boolean prettyprint;
+ private final LevelFormatter levelFormatter = LevelFormatter.of();
+
+ private static final int EXTENDED_F = 0x00000002;
+ private static final DateTimeFormatter timeFormatter = DateTimeFormatter.ISO_INSTANT;
+
+ public JsonLogAppender(
+ LogConfig config) {
+ this(
+ config.hostName(),
+ config.headers(),
+ config.defaultOutput(),
+ Optional.ofNullable(config.property("json.prettyprint"))
+ .map(p -> Boolean.valueOf(p))
+ .orElseGet(() -> false));
+ }
+
+ public JsonLogAppender(
+ String host,
+ Map headers,
+ LogEncoder out,
+ boolean prettyprint) {
+ super();
+ this.host = host;
+ this.headers = headers;
+ this.out = out;
+ this.prettyprint = prettyprint;
+ }
+
+ /*
+ * N.B. synchronized. This would be bad but outputstreams are synchronized
+ * as well .. so.
+ */
+ @Override
+ public synchronized void append(
+ LogEvent event) {
+ raw.reset();
+ final String host = this.host;
+ final String shortMessage = event.formattedMessage();
+ Instant now = event.timeStamp();
+ final double timeStamp = ((double) now.toEpochMilli()) / 1000;
+ @Nullable
+ String fullMessage = null;
+ var t = event.getThrowable();
+ if (t != null) {
+ StringWriter sw = new StringWriter();
+ sw.write(shortMessage);
+ sw.write("\n");
+ t.printStackTrace(new PrintWriter(sw));
+ fullMessage = sw.toString();
+ }
+ int level = levelToSyslogLevel(event.level());
+ raw.writeByte(OBJECT_START);
+ int index = 0;
+ index = write("host", host, index);
+ index = write("short_message", shortMessage, index);
+ index = write("full_message", fullMessage, index);
+ index = writeDouble("timestamp", timeStamp, index, 0);
+ index = writeInt("level", level, index, 0);
+ index = write("_time", timeFormatter.format(now), index);
+ index = write("_level", levelFormatter.format(event.level()), index);
+ index = write("_logger", event.loggerName(), index);
+ index = write("_thread_name", event.threadName(), index);
+ index = write("_thread_id", String.valueOf(event.threadId()), index);
+
+ if (t != null) {
+ String tn = t.getClass()
+ .getCanonicalName();
+ if (tn == null) {
+ tn = t.getClass()
+ .getName();
+ }
+ index = write("_throwable", tn, index);
+ }
+
+ var kvs = event.getKeyValues();
+
+ /*
+ * output headers
+ */
+ for (var e : headers.entrySet()) {
+ String k = e.getKey();
+ if (kvs.containsKey(k)) {
+ continue;
+ }
+ @Nullable
+ String v = e.getValue();
+ index = write(k, v, index, EXTENDED_F);
+ }
+
+ /*
+ * output MDC
+ */
+ for (var e : kvs.entrySet()) {
+ String k = e.getKey();
+ @Nullable
+ String v = e.getValue();
+ index = write(k, v, index, EXTENDED_F);
+ }
+
+ index = write("version", "1.1", index);
+ if (index > 0 && prettyprint) {
+ raw.writeAscii("\n");
+ }
+ raw.writeByte(OBJECT_END);
+ raw.writeAscii("\n");
+ raw.toStream(out);
+
+ }
+
+ private final int write(
+ String k,
+ @Nullable String v,
+ int index) {
+ return write(k, v, index, 0);
+ }
+
+ private final int write(
+ String k,
+ @Nullable String v,
+ int index,
+ int flag) {
+ if (v == null)
+ return index;
+ _writeStartField(k, index, flag);
+ raw.writeString(v);
+ _writeEndField(flag);
+ return index + 1;
+
+ }
+
+ private final int writeDouble(
+ String k,
+ double v,
+ int index,
+ int flag) {
+ _writeStartField(k, index, flag);
+ raw.writeDouble(v);
+ _writeEndField(flag);
+ return index + 1;
+ }
+
+ private final int writeInt(
+ String k,
+ int v,
+ int index,
+ int flag) {
+ _writeStartField(k, index, flag);
+ raw.writeInt(v);
+ _writeEndField(flag);
+ return index + 1;
+ }
+
+ private final void _writeStartField(
+ String k,
+ int index,
+ int flag) {
+ if (index > 0) {
+ raw.writeByte(COMMA);
+ }
+ if (prettyprint) {
+ raw.writeAscii("\n");
+ }
+ if ((flag & EXTENDED_F) == EXTENDED_F) {
+ k = "_" + k;
+ }
+ if (prettyprint) {
+ raw.writeAscii(" ");
+ }
+ raw.writeAsciiString(k);
+ raw.writeByte(SEMI);
+ }
+
+ private final void _writeEndField(
+ int flag) {
+
+ }
+
+ private int levelToSyslogLevel(
+ Level level) {
+ /*
+ * FROM LOGBACK The syslog severity of a logging event is converted from
+ * the level of the logging event. The DEBUG level is converted to 7,
+ * INFO is converted to 6, WARN is converted to 4 and ERROR is converted
+ * to 3.
+ */
+ int r = switch (level) {
+ case ERROR -> 3;
+ case DEBUG -> 7;
+ case INFO -> 6;
+ case TRACE -> 7;
+ case WARNING -> 4;
+ case ALL -> 7;
+ case OFF -> 0;
+ };
+ return r;
+ }
+
+ // protected String throwableToString(
+ // @Nullable Throwable t,
+ // @Nullable PrintStream targetStream) {
+ // if (t != null && targetStream != null) {
+ // t.printStackTrace(targetStream);
+ // }
+ // }
+
+}
diff --git a/core/src/main/java/io/jstach/rainbowgum/json/RawJsonWriter.java b/core/src/main/java/io/jstach/rainbowgum/json/RawJsonWriter.java
new file mode 100644
index 00000000..65dbb0f4
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/json/RawJsonWriter.java
@@ -0,0 +1,443 @@
+package io.jstach.rainbowgum.json;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+import io.jstach.rainbowgum.LogEncoder;
+
+public class RawJsonWriter {
+
+ private int position;
+ private byte[] buffer;
+
+ /**
+ * Helper for writing JSON object start: {
+ */
+ public static final byte OBJECT_START = '{';
+ /**
+ * Helper for writing JSON object end: }
+ */
+ public static final byte OBJECT_END = '}';
+ /**
+ * Helper for writing JSON array start: [
+ */
+ public static final byte ARRAY_START = '[';
+ /**
+ * Helper for writing JSON array end: ]
+ */
+ public static final byte ARRAY_END = ']';
+ /**
+ * Helper for writing comma separator: ,
+ */
+ public static final byte COMMA = ',';
+ /**
+ * Helper for writing semicolon: :
+ */
+ public static final byte SEMI = ':';
+ /**
+ * Helper for writing JSON quote: "
+ */
+ public static final byte QUOTE = '"';
+ /**
+ * Helper for writing JSON escape: \\
+ */
+ public static final byte ESCAPE = '\\';
+
+ private final Grisu3.FastDtoaBuilder doubleBuilder = new Grisu3.FastDtoaBuilder();
+
+ public RawJsonWriter(
+ int capacity) {
+ this.buffer = new byte[capacity];
+ }
+
+ final byte[] ensureCapacity(
+ final int free) {
+ if (position + free >= buffer.length) {
+ enlargeOrFlush(position, free);
+ }
+ return buffer;
+ }
+
+ void advance(
+ int size) {
+ position += size;
+ }
+
+ private void enlargeOrFlush(
+ final int size,
+ final int padding) {
+ buffer = Arrays.copyOf(buffer, buffer.length + buffer.length / 2 + padding);
+ }
+
+ /**
+ * Write a single byte into the JSON.
+ *
+ * @param value
+ * byte to write into the JSON
+ */
+ public final void writeByte(
+ final byte value) {
+ if (position == buffer.length) {
+ enlargeOrFlush(position, 0);
+ }
+ buffer[position++] = value;
+ }
+
+ /**
+ * Write a quoted string into the JSON. String will be appropriately escaped
+ * according to JSON escaping rules.
+ *
+ * @param value
+ * string to write
+ */
+ public final void writeString(
+ final String value) {
+ final int len = value.length();
+ if (position + (len << 2) + (len << 1) + 2 >= buffer.length) {
+ enlargeOrFlush(position, (len << 2) + (len << 1) + 2);
+ }
+ final byte[] _result = buffer;
+ _result[position] = QUOTE;
+ int cur = position + 1;
+ for (int i = 0; i < len; i++) {
+ final char c = value.charAt(i);
+ if (c > 31 && c != '"' && c != '\\' && c < 126) {
+ _result[cur++] = (byte) c;
+ }
+ else {
+ writeQuotedString(value, i, cur, len);
+ return;
+ }
+ }
+ _result[cur] = QUOTE;
+ position = cur + 1;
+ }
+
+ private void writeQuotedString(
+ final CharSequence str,
+ int i,
+ int cur,
+ final int len) {
+ final byte[] _result = this.buffer;
+ for (; i < len; i++) {
+ final char c = str.charAt(i);
+ if (c == '"') {
+ _result[cur++] = ESCAPE;
+ _result[cur++] = QUOTE;
+ }
+ else if (c == '\\') {
+ _result[cur++] = ESCAPE;
+ _result[cur++] = ESCAPE;
+ }
+ else if (c < 32) {
+ if (c == 8) {
+ _result[cur++] = ESCAPE;
+ _result[cur++] = 'b';
+ }
+ else if (c == 9) {
+ _result[cur++] = ESCAPE;
+ _result[cur++] = 't';
+ }
+ else if (c == 10) {
+ _result[cur++] = ESCAPE;
+ _result[cur++] = 'n';
+ }
+ else if (c == 12) {
+ _result[cur++] = ESCAPE;
+ _result[cur++] = 'f';
+ }
+ else if (c == 13) {
+ _result[cur++] = ESCAPE;
+ _result[cur++] = 'r';
+ }
+ else {
+ _result[cur] = ESCAPE;
+ _result[cur + 1] = 'u';
+ _result[cur + 2] = '0';
+ _result[cur + 3] = '0';
+ switch (c) {
+ case 0:
+ _result[cur + 4] = '0';
+ _result[cur + 5] = '0';
+ break;
+ case 1:
+ _result[cur + 4] = '0';
+ _result[cur + 5] = '1';
+ break;
+ case 2:
+ _result[cur + 4] = '0';
+ _result[cur + 5] = '2';
+ break;
+ case 3:
+ _result[cur + 4] = '0';
+ _result[cur + 5] = '3';
+ break;
+ case 4:
+ _result[cur + 4] = '0';
+ _result[cur + 5] = '4';
+ break;
+ case 5:
+ _result[cur + 4] = '0';
+ _result[cur + 5] = '5';
+ break;
+ case 6:
+ _result[cur + 4] = '0';
+ _result[cur + 5] = '6';
+ break;
+ case 7:
+ _result[cur + 4] = '0';
+ _result[cur + 5] = '7';
+ break;
+ case 11:
+ _result[cur + 4] = '0';
+ _result[cur + 5] = 'B';
+ break;
+ case 14:
+ _result[cur + 4] = '0';
+ _result[cur + 5] = 'E';
+ break;
+ case 15:
+ _result[cur + 4] = '0';
+ _result[cur + 5] = 'F';
+ break;
+ case 16:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = '0';
+ break;
+ case 17:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = '1';
+ break;
+ case 18:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = '2';
+ break;
+ case 19:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = '3';
+ break;
+ case 20:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = '4';
+ break;
+ case 21:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = '5';
+ break;
+ case 22:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = '6';
+ break;
+ case 23:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = '7';
+ break;
+ case 24:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = '8';
+ break;
+ case 25:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = '9';
+ break;
+ case 26:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = 'A';
+ break;
+ case 27:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = 'B';
+ break;
+ case 28:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = 'C';
+ break;
+ case 29:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = 'D';
+ break;
+ case 30:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = 'E';
+ break;
+ default:
+ _result[cur + 4] = '1';
+ _result[cur + 5] = 'F';
+ break;
+ }
+ cur += 6;
+ }
+ }
+ else if (c < 0x007F) {
+ _result[cur++] = (byte) c;
+ }
+ else {
+ final int cp = Character.codePointAt(str, i);
+ if (Character.isSupplementaryCodePoint(cp)) {
+ i++;
+ }
+ if (cp == 0x007F) {
+ _result[cur++] = (byte) cp;
+ }
+ else if (cp <= 0x7FF) {
+ _result[cur++] = (byte) (0xC0 | ((cp >> 6) & 0x1F));
+ _result[cur++] = (byte) (0x80 | (cp & 0x3F));
+ }
+ else if ((cp < 0xD800) || (cp > 0xDFFF && cp <= 0xFFFF)) {
+ _result[cur++] = (byte) (0xE0 | ((cp >> 12) & 0x0F));
+ _result[cur++] = (byte) (0x80 | ((cp >> 6) & 0x3F));
+ _result[cur++] = (byte) (0x80 | (cp & 0x3F));
+ }
+ else if (cp >= 0x10000 && cp <= 0x10FFFF) {
+ _result[cur++] = (byte) (0xF0 | ((cp >> 18) & 0x07));
+ _result[cur++] = (byte) (0x80 | ((cp >> 12) & 0x3F));
+ _result[cur++] = (byte) (0x80 | ((cp >> 6) & 0x3F));
+ _result[cur++] = (byte) (0x80 | (cp & 0x3F));
+ }
+ else {
+ throw new IllegalArgumentException(
+ "Unknown unicode codepoint in string! " + Integer.toHexString(cp));
+ }
+ }
+ }
+ _result[cur] = QUOTE;
+ position = cur + 1;
+ }
+
+ /**
+ * Write a quoted string consisting of only ascii characters. String will
+ * not be escaped according to JSON escaping rules.
+ *
+ * @param value
+ * ascii string
+ */
+ @SuppressWarnings("deprecation")
+ public final void writeAsciiString(
+ final String value) {
+ final int len = value.length() + 2;
+ if (position + len >= buffer.length) {
+ enlargeOrFlush(position, len);
+ }
+ final byte[] _result = buffer;
+ _result[position] = QUOTE;
+ value.getBytes(0, len - 2, _result, position + 1);
+ _result[position + len - 1] = QUOTE;
+ position += len;
+ }
+
+ public final void writeDouble(
+ final double value) {
+ if (value == Double.POSITIVE_INFINITY) {
+ writeAsciiString("\"Infinity\"");
+ }
+ else if (value == Double.NEGATIVE_INFINITY) {
+ writeAsciiString("\"-Infinity\"");
+ }
+ else if (value != value) {
+ writeAsciiString("\"NaN\"");
+ }
+ else if (value == 0.0) {
+ writeAsciiString("0.0");
+ }
+ else {
+ if (Grisu3.tryConvert(value, doubleBuilder)) {
+ if (position + 24 >= buffer.length) {
+ enlargeOrFlush(position, 24);
+ }
+ final int len = doubleBuilder.copyTo(buffer, position);
+ position += len;
+ }
+ else {
+ writeAsciiString(Double.toString(value));
+ }
+ }
+ }
+
+ /**
+ * Write string consisting of only ascii characters. String will not be
+ * escaped according to JSON escaping rules.
+ *
+ * @param value
+ * ascii string
+ */
+ @SuppressWarnings("deprecation")
+ public final void writeAscii(
+ final String value) {
+ final int len = value.length();
+ if (position + len >= buffer.length) {
+ enlargeOrFlush(position, len);
+ }
+ value.getBytes(0, len, buffer, position);
+ position += len;
+ }
+
+ public final void writeInt(
+ final int value) {
+ writeAscii(Integer.toString(value));
+ }
+
+ /**
+ * Content of buffer can be copied to another array of appropriate size.
+ * This method can't be used when targeting output stream. Ideally it should
+ * be avoided if possible, since it will create an array copy. It's better
+ * to use getByteBuffer and size instead.
+ *
+ * @return copy of the buffer up to the current position
+ */
+ public final byte[] toByteArray() {
+ var r = Arrays.copyOf(buffer, position);
+ reset();
+ return r;
+ }
+
+ public void toByteBuffer(
+ ByteBuffer b) {
+ b.put(buffer, 0, position);
+ b.flip();
+ reset();
+ }
+
+ /**
+ * When JsonWriter does not target stream, this method should be used to
+ * copy content of the buffer into target stream. It will also reset the
+ * buffer position to 0 so writer can be continued to be used even without a
+ * call to reset().
+ *
+ * @param stream
+ * target stream
+ * @throws IOException
+ * propagates from stream.write
+ */
+ public final void toStream(
+ final OutputStream stream)
+ throws IOException {
+ stream.write(buffer, 0, position);
+ position = 0;
+ }
+
+ public final void toStream(
+ final LogEncoder encoder) {
+ encoder.encode(buffer, 0, position);
+ position = 0;
+ }
+
+ /**
+ * Current position in the buffer. When stream is not used, this is also
+ * equivalent to the size of the resulting JSON in bytes
+ *
+ * @return position in the populated buffer
+ */
+ public final int size() {
+ return position;
+ }
+
+ /**
+ * Resets the writer
+ */
+ public final void reset() {
+ position = 0;
+ }
+
+}
diff --git a/core/src/main/java/io/jstach/rainbowgum/json/package-info.java b/core/src/main/java/io/jstach/rainbowgum/json/package-info.java
new file mode 100644
index 00000000..9fe7d321
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/json/package-info.java
@@ -0,0 +1,2 @@
+@org.eclipse.jdt.annotation.NonNullByDefault
+package io.jstach.rainbowgum.json;
\ No newline at end of file
diff --git a/core/src/main/java/io/jstach/rainbowgum/package-info.java b/core/src/main/java/io/jstach/rainbowgum/package-info.java
new file mode 100644
index 00000000..2384e2d0
--- /dev/null
+++ b/core/src/main/java/io/jstach/rainbowgum/package-info.java
@@ -0,0 +1,2 @@
+@org.eclipse.jdt.annotation.NonNullByDefault
+package io.jstach.rainbowgum;
\ No newline at end of file
diff --git a/mvnw b/mvnw
new file mode 100755
index 00000000..8d937f4c
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,308 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.2.0
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /usr/local/etc/mavenrc ] ; then
+ . /usr/local/etc/mavenrc
+ fi
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "$(uname)" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
+ else
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=$(java-config --jre-home)
+ fi
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
+ JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
+ if $darwin ; then
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
+ else
+ javaExecutable="$(readlink -f "\"$javaExecutable\"")"
+ fi
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=$(cd "$wdir/.." || exit 1; pwd)
+ fi
+ # end of workaround
+ done
+ printf '%s' "$(cd "$basedir" || exit 1; pwd)"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' < "$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
+ fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
+else
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
+ if [ -n "$MVNW_REPOURL" ]; then
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ else
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ fi
+ while IFS="=" read -r key value; do
+ # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+ safeValue=$(echo "$value" | tr -d '\r')
+ case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
+ esac
+ done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
+ if $cygwin; then
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
+ fi
+
+ if command -v wget > /dev/null; then
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ else
+ wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ else
+ curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ fi
+ else
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
+ fi
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
+ fi
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
+ esac
+done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+# shellcheck disable=SC2086 # safe args
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ $MAVEN_DEBUG_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
new file mode 100644
index 00000000..f80fbad3
--- /dev/null
+++ b/mvnw.cmd
@@ -0,0 +1,205 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.2.0
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %WRAPPER_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+ powershell -Command "&{"^
+ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+ " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+ " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+ " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+ " exit 1;"^
+ "}"^
+ "}"
+ if ERRORLEVEL 1 goto error
+)
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+ %JVM_CONFIG_MAVEN_PROPS% ^
+ %MAVEN_OPTS% ^
+ %MAVEN_DEBUG_OPTS% ^
+ -classpath %WRAPPER_JAR% ^
+ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 00000000..5dedceda
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,645 @@
+
+ 4.0.0
+ io.jstach.rainbowgum
+ rainbowgum-maven-parent
+ rainbowgum-maven-parent
+ pom
+ 0.1.3-SNAPSHOT
+ https://github.com/jstachio/rainbowgum
+ Minimum Opinionated Logging
+
+ rainbowgum
+ https://jstach.io/rainbowgum
+
+
+ 17
+ UTF-8
+ 1.11
+ 1692385133
+ snapshot
+
+ 3.9.4
+
+ 2.0.7
+
+
+
+
+ scm:git:https://github.com/jstachio/rainbowgum.git
+ scm:git:git@github.com:jstachio/rainbowgum.git
+ https://github.com/jstachio/rainbowgum/tree/main
+ HEAD
+
+
+
+ The BSD 3-Clause License
+ https://opensource.org/licenses/BSD-3-Clause
+ repo
+
+
+
+
+ agentgt
+ Adam Gent
+ adam at snaphop dot com
+ jstachio
+ https://jstach.io
+
+
+
+
+ org.eclipse.jdt
+ org.eclipse.jdt.annotation
+ true
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
+
+
+
+
+ org.apache.maven
+ maven-core
+ ${maven.core.version}
+
+
+ org.eclipse.jdt
+ org.eclipse.jdt.annotation
+ 2.2.700
+ true
+
+
+ com.samskivert
+ jmustache
+ 1.15
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.0
+ test
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ org.slf4j
+ jul-to-slf4j
+ ${slf4j.version}
+
+
+ org.slf4j
+ jcl-over-slf4j
+ ${slf4j.version}
+
+
+ org.slf4j
+ slf4j-reload4j
+ ${slf4j.version}
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
+
+
+ org.fusesource.jansi
+ jansi
+ 2.4.0
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-wrapper-plugin
+ 3.2.0
+
+
+ org.apache.maven.plugins
+ maven-clean-plugin
+ 3.3.1
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ ${project.build.sourceEncoding}
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+ ${project.build.sourceEncoding}
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.1.2
+
+ false
+ true
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-install-plugin
+ 3.1.1
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.3.0
+
+
+
+ true
+ true
+
+
+ ${buildNumber}
+ ${project.name}
+ ${project.artifactId}
+ ${project.groupId}
+ ${project.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+ 3.1.1
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.0
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.codehaus.plexus
+ plexus-archiver
+ 4.8.0
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.5.0
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.4.0
+
+
+ org.codehaus.mojo
+ buildnumber-maven-plugin
+ 3.2.0
+
+
+ validate
+
+ create
+
+
+
+
+ false
+ false
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-scm-plugin
+ 2.0.1
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.1.0
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.6.13
+
+
+ org.apache.maven.plugins
+ maven-release-plugin
+ 3.0.1
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.0
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+
+ org.moditect
+ moditect-maven-plugin
+ 1.0.0.Final
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.5.0
+
+
+ com.ruleoftech
+ markdown-page-generator-plugin
+ 2.4.0
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+ 3.3.0
+
+
+ io.spring.javaformat
+ spring-javaformat-maven-plugin
+ 0.0.39
+
+
+ org.eclipse.m2e
+ lifecycle-mapping
+ 1.0.0
+
+
+
+
+
+ io.spring.javaformat
+ spring-javaformat-maven-plugin
+ [0.0.35,)
+
+ apply
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ enforce-maven-version
+
+
+ !enforce-maven-version.disable
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+
+
+ enforce-maven
+
+ enforce
+
+
+
+
+ ${maven.core.version}
+
+
+ true
+
+
+
+
+
+
+
+
+
+ javadoc
+
+
+ !javadoc.disable
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+ public
+
+
+ apiNote
+ a
+ API Note
+
+
+ true
+
+ *.internal:*.internal.*
+
+
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+
+
+ doc
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+ public
+
+ true
+ rainbowgum API ${project.version}
+ rainbowgum API ${project.version}
+
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script><script>hljs.highlightAll();</script>
+
+
+
+
+
+ ${javadoc.stylesheet}
+
+
+
+ apiNote
+ a
+ API Note
+
+
+ --allow-script-in-comments
+ true
+
+
+
+ aggregate
+
+ aggregate
+
+ package
+
+
+
+
+
+
+
+
+
+ format-apply
+
+
+ format.apply
+
+
+
+
+
+ io.spring.javaformat
+ spring-javaformat-maven-plugin
+
+
+
+ validate
+ true
+
+ apply
+
+
+
+
+
+
+
+
+ format-validate
+
+
+ !format.apply
+
+
+
+
+
+ io.spring.javaformat
+ spring-javaformat-maven-plugin
+
+
+ validate
+ true
+
+ validate
+
+
+
+
+
+
+
+
+ deploy-snapshot
+
+
+ deploy
+ snapshot
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+
+
+ enforce-no-releases
+
+ enforce
+
+
+
+
+ No Releases Allowed!
+
+
+ true
+
+
+
+
+
+
+
+
+ deploy-release
+
+
+ deploy
+ release
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-release-plugin
+ 3.0.1
+
+ true
+ false
+ release
+ deploy
+
+
+
+
+
+
+ central
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+ public
+
+
+ apiNote
+ a
+ API Note
+
+
+ false
+
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+ *.internal:*.internal.*
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ true
+
+ ossrh
+ https://s01.oss.sonatype.org/
+ false
+
+
+
+
+
+
+ ossrh
+ https://s01.oss.sonatype.org/content/repositories/snapshots
+
+
+ ossrh
+ https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+
+
+
+ core
+ rainbowgum-slf4j
+
+
diff --git a/rainbowgum-slf4j/pom.xml b/rainbowgum-slf4j/pom.xml
new file mode 100644
index 00000000..b860324a
--- /dev/null
+++ b/rainbowgum-slf4j/pom.xml
@@ -0,0 +1,15 @@
+
+ 4.0.0
+
+ io.jstach.rainbowgum
+ rainbowgum-maven-parent
+ 0.1.3-SNAPSHOT
+
+ rainbowgum-slf4j
+
+
+ org.slf4j
+ slf4j-api
+
+
+
\ No newline at end of file
diff --git a/rainbowgum-slf4j/src/main/java/io/jstach/rainbowgum/slf4j/SimpleMDCAdapter.java b/rainbowgum-slf4j/src/main/java/io/jstach/rainbowgum/slf4j/SimpleMDCAdapter.java
new file mode 100644
index 00000000..24fac232
--- /dev/null
+++ b/rainbowgum-slf4j/src/main/java/io/jstach/rainbowgum/slf4j/SimpleMDCAdapter.java
@@ -0,0 +1,221 @@
+package io.jstach.rainbowgum.slf4j;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.spi.MDCAdapter;
+
+public class SimpleMDCAdapter implements MDCAdapter {
+
+ // The internal map is copied so as
+
+ // We wish to avoid unnecessarily copying of the map. To ensure
+ // efficient/timely copying, we have a variable keeping track of the last
+ // operation. A copy is necessary on 'put' or 'remove' but only if the last
+ // operation was a 'get'. Get operations never necessitate a copy nor
+ // successive 'put/remove' operations, only a get followed by a 'put/remove'
+ // requires copying the map.
+ // See http://jira.qos.ch/browse/LOGBACK-620 for the original discussion.
+
+ // We no longer use CopyOnInheritThreadLocal in order to solve LBCLASSIC-183
+ // Initially the contents of the thread local in parent and child threads
+ // reference the same map. However, as soon as a thread invokes the put()
+ // method, the maps diverge as they should.
+ final ThreadLocal<@Nullable Map<@NonNull String, @Nullable String>> copyOnThreadLocal = new ThreadLocal<>();
+
+ private static final int WRITE_OPERATION = 1;
+ private static final int MAP_COPY_OPERATION = 2;
+
+ // keeps track of the last operation performed
+ final ThreadLocal lastOperation = new ThreadLocal();
+
+ private Integer getAndSetLastOperation(
+ int op) {
+ Integer lastOp = lastOperation.get();
+ lastOperation.set(op);
+ return lastOp;
+ }
+
+ private boolean wasLastOpReadOrNull(
+ @Nullable Integer lastOp) {
+ return lastOp == null || lastOp.intValue() == MAP_COPY_OPERATION;
+ }
+
+ private Map<@NonNull String, @Nullable String> duplicateAndInsertNewMap(
+ @Nullable Map oldMap) {
+ Map<@NonNull String, @Nullable String> newMap = Collections.synchronizedMap(new HashMap<>());
+ if (oldMap != null) {
+ // we don't want the parent thread modifying oldMap while we are
+ // iterating over it
+ synchronized (oldMap) {
+ newMap.putAll(oldMap);
+ }
+ }
+
+ copyOnThreadLocal.set(newMap);
+ return newMap;
+ }
+
+ /**
+ * Put a context value (the val
parameter) as identified with
+ * the key
parameter into the current thread's context map.
+ * Note that contrary to log4j, the val
parameter can be null.
+ *
+ *
+ * If the current thread does not have a context map it is created as a side
+ * effect of this call.
+ *
+ * @throws NullPointerException
+ * in case the "key" parameter is null
+ */
+ public void put(
+ @NonNull String key,
+ @Nullable String val)
+ throws NullPointerException {
+ requireNonNull(key, "key cannot be null");
+
+ Map oldMap = copyOnThreadLocal.get();
+ Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);
+
+ if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
+ Map newMap = duplicateAndInsertNewMap(oldMap);
+ newMap.put(key, val);
+ }
+ else {
+ oldMap.put(key, val);
+ }
+ }
+
+ /**
+ * Remove the the context identified by the key
parameter.
+ *
+ */
+ public void remove(
+ @Nullable String key) {
+ if (key == null) {
+ return;
+ }
+ Map oldMap = copyOnThreadLocal.get();
+ if (oldMap == null)
+ return;
+
+ Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);
+
+ if (wasLastOpReadOrNull(lastOp)) {
+ Map newMap = duplicateAndInsertNewMap(oldMap);
+ newMap.remove(key);
+ }
+ else {
+ oldMap.remove(key);
+ }
+ }
+
+ /**
+ * Clear all entries in the MDC.
+ */
+ public void clear() {
+ lastOperation.set(WRITE_OPERATION);
+ copyOnThreadLocal.remove();
+ }
+
+ /**
+ * Get the context identified by the key
parameter.
+ *
+ */
+ public @Nullable String get(
+ String key) {
+ if (Objects.isNull(key)) {
+ return null;
+ }
+ final Map map = copyOnThreadLocal.get();
+ if (map != null) {
+ return map.get(key);
+ }
+ else {
+ return null;
+ }
+ }
+
+ /**
+ * Get the current thread's MDC as a map. This method is intended to be used
+ * internally.
+ */
+ public @Nullable Map getPropertyMap() {
+ lastOperation.set(MAP_COPY_OPERATION);
+ return copyOnThreadLocal.get();
+ }
+
+ /**
+ * Returns the keys in the MDC as a {@link Set}. The returned value can be
+ * null.
+ */
+ @Nullable
+ public Set getKeys() {
+ Map map = getPropertyMap();
+
+ if (map != null) {
+ return map.keySet();
+ }
+ else {
+ return null;
+ }
+ }
+
+ /**
+ * Return a copy of the current thread's context map. Returned value may be
+ * null.
+ */
+ @Nullable
+ public Map getCopyOfContextMap() {
+ Map hashMap = copyOnThreadLocal.get();
+ if (hashMap == null) {
+ return null;
+ }
+ else {
+ return new HashMap<>(hashMap);
+ }
+ }
+
+ public void setContextMap(
+ Map contextMap) {
+ lastOperation.set(WRITE_OPERATION);
+
+ Map newMap = Collections.synchronizedMap(new HashMap<>());
+ newMap.putAll(contextMap);
+
+ // the newMap replaces the old one for serialisation's sake
+ copyOnThreadLocal.set(newMap);
+ }
+
+ @Override
+ public void pushByKey(
+ String key,
+ String value) {}
+
+ @Override
+ public @Nullable String popByKey(
+ String key) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public @Nullable Deque getCopyOfDequeByKey(
+ String key) {
+ return null;
+ }
+
+ @Override
+ public void clearDequeByKey(
+ String key) {
+
+ }
+}
\ No newline at end of file
diff --git a/rainbowgum-slf4j/src/main/java/io/jstach/rainbowgum/slf4j/package-info.java b/rainbowgum-slf4j/src/main/java/io/jstach/rainbowgum/slf4j/package-info.java
new file mode 100644
index 00000000..becfcfc1
--- /dev/null
+++ b/rainbowgum-slf4j/src/main/java/io/jstach/rainbowgum/slf4j/package-info.java
@@ -0,0 +1,2 @@
+@org.eclipse.jdt.annotation.NonNullByDefault
+package io.jstach.rainbowgum.slf4j;
\ No newline at end of file