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 io.github.ascopes.jct.utils.VisibleForTestingOnly;
022import java.time.Instant;
023import java.util.List;
024import java.util.Locale;
025import java.util.concurrent.ConcurrentLinkedQueue;
026import java.util.function.Supplier;
027import java.util.stream.Collectors;
028import javax.tools.Diagnostic;
029import javax.tools.DiagnosticListener;
030import javax.tools.JavaFileObject;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033import org.slf4j.event.Level;
034
035/**
036 * A diagnostics listener that wraps all diagnostics in additional invocation information, and then
037 * stores them in a queue for processing later.
038 *
039 * @param <S> the file type.
040 * @author Ashley Scopes
041 * @since 0.0.1
042 */
043public class TracingDiagnosticListener<S extends JavaFileObject> implements DiagnosticListener<S> {
044
045  private final ConcurrentLinkedQueue<TraceDiagnostic<S>> diagnostics;
046  private final Logger logger;
047  private final Supplier<? extends Thread> threadGetter;
048  private final boolean logging;
049  private final boolean stackTraces;
050
051  /**
052   * Initialize this listener.
053   *
054   * @param logging     {@code true} if logging is enabled, {@code false} otherwise.
055   * @param stackTraces {@code true} if logging stack traces is enabled, {@code false} otherwise.
056   *                    This is ignored if {@code logging} is {@code false}.
057   */
058  public TracingDiagnosticListener(
059      boolean logging,
060      boolean stackTraces
061  ) {
062    this(
063        LoggerFactory.getLogger(TracingDiagnosticListener.class),
064        Thread::currentThread,
065        logging,
066        stackTraces
067    );
068  }
069
070  /**
071   * Only visible for testing.
072   *
073   * <p>Users should <strong>NOT</strong> use this constructor. It may be changed
074   * or removed without notice.
075   *
076   * @param logger       the logger to use.
077   * @param threadGetter the supplier of the current thread.
078   * @param logging      whether to enable logging.
079   * @param stackTraces  whether to enable stack traces in the logging.
080   */
081  @VisibleForTestingOnly
082  protected TracingDiagnosticListener(
083      Logger logger,
084      Supplier<? extends Thread> threadGetter,
085      boolean logging,
086      boolean stackTraces
087  ) {
088    diagnostics = new ConcurrentLinkedQueue<>();
089    this.logger = requireNonNull(logger, "logger");
090    this.threadGetter = requireNonNull(threadGetter, "threadGetter");
091    this.logging = logging;
092    this.stackTraces = stackTraces;
093  }
094
095  /**
096   * Determine if logging is enabled.
097   *
098   * @return {@code true} if enabled, or {@code false} if disabled.
099   */
100  public boolean isLoggingEnabled() {
101    return logging;
102  }
103
104  /**
105   * Determine if stack trace reporting is enabled.
106   *
107   * <p>Note that stack traces are only reported if {@link #isLoggingEnabled() logging is enabled}
108   * as well.
109   *
110   * @return {@code true} if enabled, or {@code false} if disabled.
111   */
112  public boolean isStackTraceReportingEnabled() {
113    return stackTraces;
114  }
115
116  /**
117   * Get a copy of the queue containing all the diagnostics that have been detected.
118   *
119   * @return the diagnostics in a list.
120   */
121  public List<TraceDiagnostic<S>> getDiagnostics() {
122    return List.copyOf(diagnostics);
123  }
124
125  @Override
126  public final void report(Diagnostic<? extends S> diagnostic) {
127    requireNonNull(diagnostic);
128
129    var now = Instant.now();
130    var thisThread = threadGetter.get();
131    var threadName = thisThread.getName();
132    var stackTrace = List.of(thisThread.getStackTrace());
133    var threadId = LoomPolyfill.getThreadId(thisThread);
134
135    var wrapped = new TraceDiagnostic<S>(now, threadId, threadName, stackTrace, diagnostic);
136
137    diagnostics.add(wrapped);
138
139    if (!logging) {
140      return;
141    }
142
143    logger
144        .atLevel(diagnosticToLevel(diagnostic))
145        .setMessage("{}{}")
146        .addArgument(messageGetter(wrapped))
147        .addArgument(stackTraceFormatter(stackTrace))
148        .log();
149  }
150
151  private Level diagnosticToLevel(Diagnostic<?> diagnostic) {
152    return switch (diagnostic.getKind()) {
153      case ERROR -> Level.ERROR;
154      case WARNING, MANDATORY_WARNING -> Level.WARN;
155      default -> Level.INFO;
156    };
157  }
158
159  private Supplier<String> messageGetter(Diagnostic<?> diagnostic) {
160    return () -> diagnostic.getMessage(Locale.ROOT);
161  }
162
163  private Supplier<String> stackTraceFormatter(List<StackTraceElement> stackTrace) {
164    if (!stackTraces) {
165      return () -> "";
166    }
167
168    return () -> stackTrace
169        .stream()
170        .map(frame -> "\n\t" + frame)
171        .collect(Collectors.joining());
172  }
173}