From 64a30dfa075b5ed001d3e1050e9317eb524f3e51 Mon Sep 17 00:00:00 2001 From: Adam Gent Date: Thu, 14 Sep 2023 17:17:19 -0400 Subject: [PATCH] Initial logger wrapper --- .github/dependabot.yml | 7 + .github/workflows/apidoc.yml | 49 + .github/workflows/maven.yml | 39 + .github/workflows/test-report.yml | 16 + .gitignore | 45 + .mvn/wrapper/maven-wrapper.properties | 18 + core/pom.xml | 17 + .../io/jstach/rainbowgum/LogAppender.java | 20 + .../java/io/jstach/rainbowgum/LogConfig.java | 49 + .../java/io/jstach/rainbowgum/LogEncoder.java | 48 + .../java/io/jstach/rainbowgum/LogEvent.java | 59 + .../io/jstach/rainbowgum/LogFormatter.java | 55 + .../java/io/jstach/rainbowgum/LogOutput.java | 54 + .../java/io/jstach/rainbowgum/LogPlugin.java | 20 + .../java/io/jstach/rainbowgum/LogRouter.java | 125 ++ .../java/io/jstach/rainbowgum/RainbowGum.java | 51 + .../rainbowgum/jansi/JansiLogFormatter.java | 198 ++++ .../jstach/rainbowgum/jansi/package-info.java | 2 + .../io/jstach/rainbowgum/json/Grisu3.java | 1056 +++++++++++++++++ .../rainbowgum/json/JsonLogAppender.java | 244 ++++ .../jstach/rainbowgum/json/RawJsonWriter.java | 443 +++++++ .../jstach/rainbowgum/json/package-info.java | 2 + .../io/jstach/rainbowgum/package-info.java | 2 + mvnw | 308 +++++ mvnw.cmd | 205 ++++ pom.xml | 645 ++++++++++ rainbowgum-slf4j/pom.xml | 15 + .../rainbowgum/slf4j/SimpleMDCAdapter.java | 221 ++++ .../jstach/rainbowgum/slf4j/package-info.java | 2 + 29 files changed, 4015 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/apidoc.yml create mode 100644 .github/workflows/maven.yml create mode 100644 .github/workflows/test-report.yml create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 core/pom.xml create mode 100644 core/src/main/java/io/jstach/rainbowgum/LogAppender.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/LogConfig.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/LogEncoder.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/LogEvent.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/LogFormatter.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/LogOutput.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/LogPlugin.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/LogRouter.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/RainbowGum.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/jansi/JansiLogFormatter.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/jansi/package-info.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/json/Grisu3.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/json/JsonLogAppender.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/json/RawJsonWriter.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/json/package-info.java create mode 100644 core/src/main/java/io/jstach/rainbowgum/package-info.java create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 rainbowgum-slf4j/pom.xml create mode 100644 rainbowgum-slf4j/src/main/java/io/jstach/rainbowgum/slf4j/SimpleMDCAdapter.java create mode 100644 rainbowgum-slf4j/src/main/java/io/jstach/rainbowgum/slf4j/package-info.java 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 appenders) { + return new DefaultLogRouter(appenders); + } +} + +class DefaultLogRouter implements LogRouter { + + private final List logAppenders; + + public DefaultLogRouter( + List 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