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