001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.logging;
018
019import java.io.*;
020import java.util.*;
021import java.util.function.*;
022import java.util.logging.Handler;
023import java.util.regex.*;
024
025import org.apache.juneau.commons.function.*;
026
027/**
028 * Captures log records for testing purposes.
029 *
030 * <p>
031 * This class implements {@link LogRecordListener} to receive log records and stores them
032 * in memory for inspection during tests. It implements {@link Closeable} to automatically
033 * remove itself from the logger's listeners when closed.
034 *
035 * <h5 class='section'>Usage:</h5>
036 * <p class='bjava'>
037 *    <jc>// Capture records using try-with-resources</jc>
038 *    <jk>try</jk> (LogRecordCapture <jv>capture</jv> = RichLogger.getLogger(MyClass.<jk>class</jk>).captureEvents()) {
039 *       <jv>logger</jv>.info(<js>"Test message"</js>);
040 *       <jv>logger</jv>.warning(<js>"Warning message"</js>);
041 *
042 *       <jc>// Inspect captured records</jc>
043 *       List&lt;LogRecord&gt; <jv>records</jv> = <jv>capture</jv>.getRecords();
044 *       assertEquals(2, <jv>records</jv>.size());
045 *
046 *       <jc>// Get formatted messages</jc>
047 *       List&lt;String&gt; <jv>messages</jv> = <jv>capture</jv>.getRecords(<js>"{level}: {msg}"</js>);
048 *       assertEquals(<js>"INFO: Test message"</js>, <jv>messages</jv>.get(0));
049 *    }
050 * </p>
051 *
052 * <h5 class='section'>Format String:</h5>
053 * <p>
054 * The format string supports placeholders as documented in {@link LogRecord#formatted(String)}.
055 * </p>
056 *
057 * <h5 class='section'>See Also:</h5><ul>
058 *    <li class='jc'>{@link RichLogger#captureEvents()}
059 *    <li class='jc'>{@link LogRecord#formatted(String)}
060 * </ul>
061 */
062public class LogRecordCapture implements LogRecordListener, Closeable {
063
064   /**
065    * Runs a block while capturing, and keeping off the console, everything logged to the named logger or any of its descendants.
066    * <p>
067    * Unlike {@link RichLogger#captureEvents()}, which only observes records, this also stops the logger's records from
068    * reaching the parent (console) handlers for the duration of the block.  The logger's previous parent-handler setting
069    * is restored in a <c>finally</c> block.
070    * <p class='bjava'>
071    *    List&lt;LogRecord&gt; <jv>records</jv> = LogRecordCapture.<jsm>quietly</jsm>(MyClass.<jk>class</jk>, () -&gt; <jv>obj</jv>.doSomethingThatWarns());
072    *    assertEquals(1, <jv>records</jv>.size());
073    * </p>
074    *
075    * @param loggerName The logger name.  Records from descendant loggers are captured too.
076    * @param body The code to run.  Checked exceptions are rethrown wrapped in a {@link RuntimeException}.
077    * @return The captured records, in the order logged.  Never <jk>null</jk>.
078    */
079   @SuppressWarnings({
080      "java:S1181" // Snippet.run() throws Throwable; Errors are rethrown and checked exceptions wrapped
081   })
082   public static List<java.util.logging.LogRecord> quietly(String loggerName, Snippet body) {
083      var records = Collections.synchronizedList(new ArrayList<java.util.logging.LogRecord>());
084      var handler = new Handler() {
085         @Override public void publish(java.util.logging.LogRecord r) { records.add(r); }
086         @Override public void flush() { /* No-op. */ }
087         @Override public void close() { /* No-op. */ }
088      };
089      var log = java.util.logging.Logger.getLogger(loggerName);
090      try (var q = quiet(loggerName)) {
091         log.addHandler(handler);
092         try {
093            body.run();
094         } catch (RuntimeException | Error e) {
095            throw e;
096         } catch (Throwable e) {
097            throw new RuntimeException(e);
098         } finally {
099            log.removeHandler(handler);
100         }
101      }
102      synchronized (records) {
103         return List.copyOf(records);
104      }
105   }
106
107   /**
108    * Keeps everything logged to the named logger, and its descendants, off the console until the returned handle is closed.
109    * <p>
110    * Use this when a whole test class (for example from <ja>@BeforeAll</ja> / <ja>@AfterAll</ja>) expects a warning it is not
111    * asserting on.  Calls nest and may overlap across threads: the logger's original parent-handler setting is restored when
112    * the last outstanding handle for that logger name is closed.
113    *
114    * @param loggerName The logger name.
115    * @return A handle that restores the logger when closed.  Never <jk>null</jk>.
116    */
117   public static Quiet quiet(String loggerName) {
118      synchronized (QUIET) {
119         var state = QUIET.computeIfAbsent(loggerName, k -> new QuietState(java.util.logging.Logger.getLogger(k)));
120         if (state.count++ == 0) {
121            state.useParent = state.log.getUseParentHandlers();
122            state.log.setUseParentHandlers(false);
123         }
124         return () -> release(loggerName);
125      }
126   }
127
128   /**
129    * Same as {@link #quiet(String)}, keyed by the logger's class name.
130    *
131    * @param loggerClass The class whose logger is silenced.
132    * @return A handle that restores the logger when closed.  Never <jk>null</jk>.
133    */
134   @SuppressWarnings({
135      "resource" // The returned handle is owned by the caller, who closes it.
136   })
137   public static Quiet quiet(Class<?> loggerClass) {
138      return quiet(loggerClass.getName());
139   }
140
141   /**
142    * Handle returned by {@link #quiet(String)}.
143    */
144   @FunctionalInterface
145   public interface Quiet extends AutoCloseable {
146      @Override void close();
147   }
148
149   private static final Map<String,QuietState> QUIET = new HashMap<>();
150
151   private static final class QuietState {
152      final java.util.logging.Logger log;  // Strong reference: the LogManager only holds loggers weakly.
153      int count;
154      boolean useParent;
155      QuietState(java.util.logging.Logger log) { this.log = log; }
156   }
157
158   private static void release(String loggerName) {
159      synchronized (QUIET) {
160         var state = QUIET.get(loggerName);
161         if (--state.count == 0) {
162            state.log.setUseParentHandlers(state.useParent);
163            QUIET.remove(loggerName);
164         }
165      }
166   }
167
168   /**
169    * Same as {@link #quietly(String, Snippet)}, keyed by the logger's class name.
170    *
171    * @param loggerClass The class whose logger is captured.
172    * @param body The code to run.
173    * @return The captured records, in the order logged.  Never <jk>null</jk>.
174    */
175   public static List<java.util.logging.LogRecord> quietly(Class<?> loggerClass, Snippet body) {
176      return quietly(loggerClass.getName(), body);
177   }
178
179   private final RichLogger logger;
180   private final List<RichLogger> pinnedLoggers = new ArrayList<>();
181   private final List<java.util.logging.LogRecord> records = Collections.synchronizedList(new ArrayList<>());
182   private final Predicate<java.util.logging.LogRecord> filter;
183
184   /**
185    * Constructor.
186    *
187    * @param logger The logger to capture records from.
188    */
189   LogRecordCapture(RichLogger logger) {
190      this(logger, x -> true);
191   }
192
193   LogRecordCapture(RichLogger logger, Predicate<java.util.logging.LogRecord> filter) {
194      this.logger = logger;
195      this.filter = filter == null ? x -> true : filter;
196      pinnedLoggers.add(logger);
197      RichLogger.forEachLiveAncestor(logger.getName(), pinnedLoggers::add);
198      logger.addLogRecordListener(this);
199   }
200
201   /**
202    * Called when a log record is logged.
203    *
204    * @param rec The log record that was logged.
205    */
206   @Override
207   public void onLogRecord(java.util.logging.LogRecord rec) {
208      if (filter.test(rec))
209         records.add(rec);
210   }
211
212   /**
213    * Returns an unmodifiable list of all captured log records.
214    *
215    * @return An unmodifiable list of captured LogRecords.
216    */
217   public List<java.util.logging.LogRecord> getRecords() {
218      synchronized (records) {
219         return List.copyOf(records);
220      }
221   }
222
223   /**
224    * Returns captured log records formatted as strings.
225    *
226    * <p>
227    * The format string supports placeholders as documented in {@link LogRecord#formatted(String)}.
228    *
229    * @param format The format string with placeholders.
230    * @return A list of formatted record strings.
231    */
232   public List<String> getRecords(String format) {
233      synchronized (records) {
234         return records.stream()
235            .map(x -> LogRecord.formatted(x, format))
236            .toList();
237      }
238   }
239
240   /**
241    * Clears all captured records.
242    */
243   public void clear() {
244      records.clear();
245   }
246
247   /**
248    * Returns the number of captured records.
249    *
250    * @return The number of captured records.
251    */
252   public int size() {
253      return records.size();
254   }
255
256   /**
257    * Returns <jk>true</jk> if any records have been captured.
258    *
259    * @return <jk>true</jk> if records have been captured.
260    */
261   public boolean isEmpty() {
262      return records.isEmpty();
263   }
264
265   public List<String> messages() {
266      synchronized (records) {
267         return records.stream().map(java.util.logging.LogRecord::getMessage).toList();
268      }
269   }
270
271   public java.util.logging.LogRecord last() {
272      synchronized (records) {
273         return records.isEmpty() ? null : records.get(records.size() - 1);
274      }
275   }
276
277   public List<java.util.logging.LogRecord> byLevel(java.util.logging.Level level) {
278      synchronized (records) {
279         return records.stream().filter(x -> x.getLevel().equals(level)).toList();
280      }
281   }
282
283   public List<java.util.logging.LogRecord> matching(String regex) {
284      var p = Pattern.compile(regex);
285      synchronized (records) {
286         return records.stream().filter(x -> p.matcher(x.getMessage()).find()).toList();
287      }
288   }
289
290   /**
291    * Returns the value of a {@link LogContext} entry attached to the specified captured record.
292    *
293    * <p>
294    * Convenience over {@link LogRecordContext#of(java.util.logging.LogRecord)} so capture-based tests do not need to
295    * know about the side table directly.
296    *
297    * @param rec The captured record.
298    * @param key The context key.
299    * @return The attached value for the key, or <jk>null</jk> if not present.
300    */
301   public Object contextValue(java.util.logging.LogRecord rec, String key) {
302      return LogRecordContext.of(rec).get(key);
303   }
304
305   public String assertMessage() {
306      var r = last();
307      return r == null ? null : r.getMessage();
308   }
309
310   public java.util.logging.Level assertLevel() {
311      var r = last();
312      return r == null ? null : r.getLevel();
313   }
314
315   public Throwable assertThrown() {
316      var r = last();
317      return r == null ? null : r.getThrown();
318   }
319
320   /**
321    * Closes this capture and removes it from the logger's listeners.
322    */
323   @Override
324   public void close() {
325      logger.removeLogRecordListener(this);
326      pinnedLoggers.clear();
327   }
328}