001/*
002 * Copyright 2023 the original author or authors.
003 * <p>
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 * <p>
008 * https://www.apache.org/licenses/LICENSE-2.0
009 * <p>
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 de.cuioss.test.juli;
017
018import static de.cuioss.tools.string.MoreStrings.isEmpty;
019import static org.junit.jupiter.api.Assertions.assertFalse;
020import static org.junit.jupiter.api.Assertions.assertNotNull;
021
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.Comparator;
025import java.util.List;
026import java.util.logging.Handler;
027import java.util.logging.LogRecord;
028
029import lombok.Getter;
030
031/**
032 * Handler for storing and querying {@link LogRecord}s
033 *
034 * @author Oliver Wolff
035 *
036 */
037public class TestLogHandler extends Handler {
038
039    private static final String TEST_LOG_LEVEL_MUST_NOT_BE_NULL = "TestLogLevel must not be null";
040
041    private static final String LOGGER_MUST_NOT_BE_NULL = "Logger must not be null";
042
043    private static final String MESSAGE_MUST_NOT_BE_NULL = "Message must not be null";
044
045    private static final String THROWABLE_CLASS_MUST_NOT_BE_NULL = "ThrowableClass must not be null";
046
047    private static final String THROWABLE_MUST_NOT_BE_NULL = "Throwable must not be null";
048
049    @Getter
050    private final List<LogRecord> records = Collections.synchronizedList(new ArrayList<>());
051
052    @Override
053    public void publish(LogRecord logRecord) {
054        // Silently ignore null records.
055        if (logRecord == null) {
056            return;
057        }
058        records.add(logRecord);
059
060    }
061
062    @Override
063    public void close() {
064        // There is no need to close
065    }
066
067    @Override
068    public void flush() {
069        // There is no need to flush
070    }
071
072    /**
073     * @param level          to be checked for message, must not be null
074     * @param message        to be checked, must not be null
075     * @param throwableClass to be checked, must not be null
076     * @return a {@link List} of found {@link LogRecord}s
077     */
078    public List<LogRecord> resolveLogMessages(TestLogLevel level, String message,
079            Class<? extends Throwable> throwableClass) {
080        assertNotNull(throwableClass, THROWABLE_CLASS_MUST_NOT_BE_NULL);
081        return resolveLogMessages(level, message).stream().filter(r -> logRecordContains(r, throwableClass)).toList();
082    }
083
084    /**
085     * @param level     to be checked for message, must not be null
086     * @param message   to be checked, must not be null
087     * @param throwable to be checked, must not be null
088     * @return a {@link List} of found {@link LogRecord}s
089     */
090    public List<LogRecord> resolveLogMessages(TestLogLevel level, String message, Throwable throwable) {
091        assertNotNull(throwable, THROWABLE_MUST_NOT_BE_NULL);
092        return resolveLogMessages(level, message).stream().filter(r -> logRecordContains(r, throwable)).toList();
093    }
094
095    /**
096     * @param level   to be checked for message, must not be null
097     * @param message to be checked, must not be null
098     * @return a {@link List} of found {@link LogRecord}s
099     */
100    public List<LogRecord> resolveLogMessages(TestLogLevel level, String message) {
101        assertNotNull(message, MESSAGE_MUST_NOT_BE_NULL);
102        return resolveLogMessages(level).stream().filter(r -> message.equals(r.getMessage())).toList();
103    }
104
105    /**
106     * @param logger to be checked, must not be null
107     * @return a {@link List} of found {@link LogRecord}s
108     */
109    public List<LogRecord> resolveLogMessagesForLogger(String logger) {
110        assertFalse(isEmpty(logger), LOGGER_MUST_NOT_BE_NULL);
111        return records.stream().filter(r -> logger.equalsIgnoreCase(r.getLoggerName())).toList();
112    }
113
114    /**
115     * @param level  to be checked for message, must not be null
116     * @param logger to be checked, must not be null
117     * @return a {@link List} of found {@link LogRecord}s
118     */
119    public List<LogRecord> resolveLogMessagesForLogger(TestLogLevel level, String logger) {
120        assertNotNull(level, TEST_LOG_LEVEL_MUST_NOT_BE_NULL);
121        return resolveLogMessagesForLogger(logger).stream().filter(r -> level.getJuliLevel().equals(r.getLevel()))
122                .toList();
123    }
124
125    /**
126     * @param level  to be checked for message, must not be null
127     * @param logger to be checked, must not be null
128     * @return a {@link List} of found {@link LogRecord}s
129     */
130    public List<LogRecord> resolveLogMessagesForLogger(TestLogLevel level, Class<?> logger) {
131        assertNotNull(level, TEST_LOG_LEVEL_MUST_NOT_BE_NULL);
132        assertNotNull(logger, LOGGER_MUST_NOT_BE_NULL);
133        return resolveLogMessagesForLogger(level, logger.getName());
134    }
135
136    /**
137     * @param logger to be checked, must not be null
138     * @return a {@link List} of found {@link LogRecord}s
139     */
140    public List<LogRecord> resolveLogMessagesForLogger(Class<?> logger) {
141        assertNotNull(logger, LOGGER_MUST_NOT_BE_NULL);
142        return resolveLogMessagesForLogger(logger.getName());
143    }
144
145    /**
146     * @param level       to be checked for message, must not be null
147     * @param messagePart to be checked, must not be null. Compared to
148     *                    {@link TestLogHandler#resolveLogMessages(TestLogLevel, String)}
149     *                    this method check whether the given text is contained
150     *                    within a {@link LogRecord}
151     * @return a {@link List} of found {@link LogRecord}s
152     */
153    public List<LogRecord> resolveLogMessagesContaining(TestLogLevel level, String messagePart) {
154        assertNotNull(messagePart, MESSAGE_MUST_NOT_BE_NULL);
155        return resolveLogMessages(level).stream().filter(r -> logRecordContains(r, messagePart)).toList();
156    }
157
158    /**
159     * @param level       to be checked for message, must not be null
160     * @param messagePart to be checked, must not be null. Compared to
161     *                    {@link TestLogHandler#resolveLogMessages(TestLogLevel, String)}
162     *                    this method check whether the given text is contained
163     *                    within a {@link LogRecord}
164     * @param throwable   to be looked for
165     * @return a {@link List} of found {@link LogRecord}s
166     */
167    public List<LogRecord> resolveLogMessagesContaining(TestLogLevel level, String messagePart, Throwable throwable) {
168        assertNotNull(throwable, THROWABLE_MUST_NOT_BE_NULL);
169        return resolveLogMessagesContaining(level, messagePart).stream().filter(r -> logRecordContains(r, throwable))
170                .toList();
171    }
172
173    /**
174     * @param level          to be checked for message, must not be null
175     * @param messagePart    to be checked, must not be null. Compared to
176     *                       {@link TestLogHandler#resolveLogMessages(TestLogLevel, String)}
177     *                       this method check whether the given text is contained
178     *                       within a {@link LogRecord}
179     * @param throwableClass to be looked for
180     * @return a {@link List} of found {@link LogRecord}s
181     */
182    public List<LogRecord> resolveLogMessagesContaining(TestLogLevel level, String messagePart,
183            Class<? extends Throwable> throwableClass) {
184        assertNotNull(throwableClass, THROWABLE_CLASS_MUST_NOT_BE_NULL);
185        return resolveLogMessagesContaining(level, messagePart).stream()
186                .filter(r -> logRecordContains(r, throwableClass)).toList();
187    }
188
189    /**
190     * @param level to be checked for message, must not be null
191     * @return a {@link List} of found {@link LogRecord}s
192     */
193    public List<LogRecord> resolveLogMessages(TestLogLevel level) {
194        assertNotNull(level, TEST_LOG_LEVEL_MUST_NOT_BE_NULL);
195        synchronized (records) {
196            return records.stream().filter(r -> logRecordContains(r, level)).toList();
197        }
198    }
199
200    /**
201     * Clears the contained records
202     */
203    public void clearRecords() {
204        records.clear();
205    }
206
207    private static boolean logRecordContains(LogRecord logRecord, String messagePart) {
208        final var msg = logRecord.getMessage();
209        return null != msg && msg.contains(messagePart);
210    }
211
212    private static boolean logRecordContains(LogRecord logRecord, Class<? extends Throwable> throwableClass) {
213        var thrown = logRecord.getThrown();
214        return null != thrown && thrown.getClass().equals(throwableClass);
215    }
216
217    private static boolean logRecordContains(LogRecord logRecord, Throwable throwable) {
218        var thrown = logRecord.getThrown();
219        return null != thrown && thrown.equals(throwable);
220    }
221
222    private static boolean logRecordContains(LogRecord logRecord, TestLogLevel level) {
223        var loggedLevel = logRecord.getLevel();
224        return null != loggedLevel && loggedLevel.equals(level.getJuliLevel());
225    }
226
227    @Override
228    public String toString() {
229        return getClass().getName() + " with " + getRecords().size() + " entries";
230    }
231
232    /**
233     * @return String representation of the records within this handler.
234     */
235    public String getRecordsAsString() {
236        if (records.isEmpty()) {
237            return "No log messages available";
238        }
239        List<LogRecord> all = new ArrayList<>(records);
240
241        all.sort(Comparator.comparing(l -> l.getLevel().intValue()));
242
243        List<String> elements = new ArrayList<>();
244
245        all.forEach(
246                l -> elements.add(TestLogLevel.parse(l.getLevel()) + ": " + l.getLoggerName() + "-" + l.getMessage()));
247        var builder = new StringBuilder();
248        builder.append("Available Messages:");
249        for (String element : elements) {
250            builder.append("\n").append(element);
251        }
252        return builder.toString();
253    }
254}