001/*
002 * Copyright (C) 2022 - 2025, the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *    http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.github.ascopes.jct.diagnostics;
017
018import static java.util.Objects.requireNonNull;
019
020import io.github.ascopes.jct.utils.LoomPolyfill;
021import java.time.Instant;
022import java.util.List;
023import java.util.Locale;
024import java.util.concurrent.ConcurrentLinkedQueue;
025import java.util.function.Supplier;
026import java.util.stream.Collectors;
027import javax.tools.Diagnostic;
028import javax.tools.DiagnosticListener;
029import javax.tools.JavaFileObject;
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032import org.slf4j.event.Level;
033
034/**
035 * A diagnostics listener that wraps all diagnostics in additional invocation information, and then
036 * stores them in a queue for processing later.
037 *
038 * @param <S> the file type.
039 * @author Ashley Scopes
040 * @since 0.0.1
041 */
042public class TracingDiagnosticListener<S extends JavaFileObject> implements DiagnosticListener<S> {
043
044  private final ConcurrentLinkedQueue<TraceDiagnostic<S>> diagnostics;
045  private final Logger logger;
046  private final Supplier<? extends Thread> threadGetter;
047  private final boolean logging;
048  private final boolean stackTraces;
049
050  /**
051   * Initialize this listener.
052   *
053   * @param logging     {@code true} if logging is enabled, {@code false} otherwise.
054   * @param stackTraces {@code true} if logging stack traces is enabled, {@code false} otherwise.
055   *                    This is ignored if {@code logging} is {@code false}.
056   */
057  public TracingDiagnosticListener(
058      boolean logging,
059      boolean stackTraces
060  ) {
061    this(
062        LoggerFactory.getLogger(TracingDiagnosticListener.class),
063        Thread::currentThread,
064        logging,
065        stackTraces
066    );
067  }
068
069  TracingDiagnosticListener(
070      Logger logger,
071      Supplier<? extends Thread> threadGetter,
072      boolean logging,
073      boolean stackTraces
074  ) {
075    diagnostics = new ConcurrentLinkedQueue<>();
076    this.logger = requireNonNull(logger, "logger");
077    this.threadGetter = requireNonNull(threadGetter, "threadGetter");
078    this.logging = logging;
079    this.stackTraces = stackTraces;
080  }
081
082  /**
083   * Determine if logging is enabled.
084   *
085   * @return {@code true} if enabled, or {@code false} if disabled.
086   */
087  public boolean isLoggingEnabled() {
088    return logging;
089  }
090
091  /**
092   * Determine if stack trace reporting is enabled.
093   *
094   * <p>Note that stack traces are only reported if {@link #isLoggingEnabled() logging is enabled}
095   * as well.
096   *
097   * @return {@code true} if enabled, or {@code false} if disabled.
098   */
099  public boolean isStackTraceReportingEnabled() {
100    return stackTraces;
101  }
102
103  /**
104   * Get a copy of the queue containing all the diagnostics that have been detected.
105   *
106   * @return the diagnostics in a list.
107   */
108  public List<TraceDiagnostic<S>> getDiagnostics() {
109    return List.copyOf(diagnostics);
110  }
111
112  @Override
113  public final void report(Diagnostic<? extends S> diagnostic) {
114    requireNonNull(diagnostic);
115
116    var now = Instant.now();
117    var thisThread = threadGetter.get();
118    var threadName = thisThread.getName();
119    var stackTrace = List.of(thisThread.getStackTrace());
120    var threadId = LoomPolyfill.getThreadId(thisThread);
121
122    var wrapped = new TraceDiagnostic<S>(now, threadId, threadName, stackTrace, diagnostic);
123
124    diagnostics.add(wrapped);
125
126    if (!logging) {
127      return;
128    }
129
130    logger
131        .atLevel(diagnosticToLevel(diagnostic))
132        .setMessage("{}{}")
133        .addArgument(messageGetter(wrapped))
134        .addArgument(stackTraceFormatter(stackTrace))
135        .log();
136  }
137
138  private Level diagnosticToLevel(Diagnostic<?> diagnostic) {
139    return switch (diagnostic.getKind()) {
140      case ERROR -> Level.ERROR;
141      case WARNING, MANDATORY_WARNING -> Level.WARN;
142      default -> Level.INFO;
143    };
144  }
145
146  private Supplier<String> messageGetter(Diagnostic<?> diagnostic) {
147    return () -> diagnostic.getMessage(Locale.ROOT);
148  }
149
150  private Supplier<String> stackTraceFormatter(List<StackTraceElement> stackTrace) {
151    if (!stackTraces) {
152      return () -> "";
153    }
154
155    return () -> stackTrace
156        .stream()
157        .map(frame -> "\n\t" + frame)
158        .collect(Collectors.joining());
159  }
160}