TestLogger.java

package uk.org.lidalia.slf4jtest;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

import org.slf4j.Logger;
import org.slf4j.MDC;
import org.slf4j.Marker;
import org.slf4j.helpers.FormattingTuple;
import org.slf4j.helpers.MessageFormatter;

import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;

import uk.org.lidalia.lang.ThreadLocal;
import uk.org.lidalia.slf4jext.Level;
import static com.google.common.base.Optional.fromNullable;
import static com.google.common.base.Optional.of;
import static com.google.common.collect.ImmutableList.copyOf;
import static com.google.common.collect.Sets.immutableEnumSet;
import static java.util.Arrays.asList;
import static uk.org.lidalia.slf4jext.Level.DEBUG;
import static uk.org.lidalia.slf4jext.Level.ERROR;
import static uk.org.lidalia.slf4jext.Level.INFO;
import static uk.org.lidalia.slf4jext.Level.TRACE;
import static uk.org.lidalia.slf4jext.Level.WARN;
import static uk.org.lidalia.slf4jext.Level.enablableValueSet;

/**
 * Implementation of {@link Logger} which stores {@link LoggingEvent}s in memory and provides methods
 * to access and remove them in order to facilitate writing tests that assert particular logging calls were made.
 * <p/>
 * {@link LoggingEvent}s are stored in both an {@link ThreadLocal} and a normal {@link List}. The {@link #getLoggingEvents()}
 * and {@link #clear()} methods reference the {@link ThreadLocal} events. The {@link #getAllLoggingEvents()} and
 * {@link #clearAll()} methods reference all events logged on this Logger.  This is in order to facilitate parallelising
 * tests - tests that use the thread local methods can be parallelised.
 * <p/>
 * By default all Levels are enabled.  It is important to note that the conventional hierarchical notion of Levels, where
 * info being enabled implies warn and error being enabled, is not a requirement of the SLF4J API, so the
 * {@link #setEnabledLevels(ImmutableSet)}, {@link #setEnabledLevels(Level...)},
 * {@link #setEnabledLevelsForAllThreads(ImmutableSet)}, {@link #setEnabledLevelsForAllThreads(Level...)} and the various
 * isXxxxxEnabled() methods make no assumptions about this hierarchy.  If you wish to use traditional hierarchical setups you may
 * do so by passing the constants in {@link uk.org.lidalia.slf4jext.ConventionalLevelHierarchy} to
 * {@link #setEnabledLevels(ImmutableSet)} or {@link #setEnabledLevelsForAllThreads(ImmutableSet)}.
 */
@SuppressWarnings({ "PMD.ExcessivePublicCount", "PMD.TooManyMethods" })
public class TestLogger implements Logger {

    private final String name;
    private final TestLoggerFactory testLoggerFactory;
    private final ThreadLocal<List<LoggingEvent>> loggingEvents = new ThreadLocal<>(
            Suppliers.<LoggingEvent>makeEmptyMutableList());

    private final List<LoggingEvent> allLoggingEvents = new CopyOnWriteArrayList<>();
    private volatile ThreadLocal<ImmutableSet<Level>> enabledLevels = new ThreadLocal<>(enablableValueSet());

    TestLogger(final String name, final TestLoggerFactory testLoggerFactory) {
        this.name = name;
        this.testLoggerFactory = testLoggerFactory;
    }

    public String getName() {
        return name;
    }

    /**
     * Removes all {@link LoggingEvent}s logged by this thread and resets the enabled levels of the logger
     * to {@link uk.org.lidalia.slf4jext.Level#enablableValueSet()} for this thread.
     */
    public void clear() {
        loggingEvents.get().clear();
        enabledLevels.remove();
    }

    /**
     * Removes ALL {@link LoggingEvent}s logged on this logger, regardless of thread,
     * and resets the enabled levels of the logger to {@link uk.org.lidalia.slf4jext.Level#enablableValueSet()}
     * for ALL threads.
     */
    public void clearAll() {
        allLoggingEvents.clear();
        loggingEvents.reset();
        enabledLevels.reset();
    }

    /**
     * @return all {@link LoggingEvent}s logged on this logger by this thread
     */
    public ImmutableList<LoggingEvent> getLoggingEvents() {
        return copyOf(loggingEvents.get());
    }

    /**
     * @return all {@link LoggingEvent}s logged on this logger by ANY thread
     */
    public ImmutableList<LoggingEvent> getAllLoggingEvents() {
        return copyOf(allLoggingEvents);
    }

    /**
     * @return whether this logger is trace enabled in this thread
     */
    @Override
    public boolean isTraceEnabled() {
        return enabledLevels.get().contains(TRACE);
    }

    @Override
    public void trace(final String message) {
        log(TRACE, message);
    }

    @Override
    public void trace(final String format, final Object arg) {
        log(TRACE, format, arg);
    }

    @Override
    public void trace(final String format, final Object arg1, final Object arg2) {
        log(TRACE, format, arg1, arg2);
    }

    @Override
    public void trace(final String format, final Object... args) {
        log(TRACE, format, args);
    }

    @Override
    public void trace(final String msg, final Throwable throwable) {
        log(TRACE, msg, throwable);
    }

    @Override
    public boolean isTraceEnabled(final Marker marker) {
        return enabledLevels.get().contains(TRACE);
    }

    @Override
    public void trace(final Marker marker, final String msg) {
        log(TRACE, marker, msg);
    }

    @Override
    public void trace(final Marker marker, final String format, final Object arg) {
        log(TRACE, marker, format, arg);
    }

    @Override
    public void trace(final Marker marker, final String format, final Object arg1, final Object arg2) {
        log(TRACE, marker, format, arg1, arg2);
    }

    @Override
    public void trace(final Marker marker, final String format, final Object... args) {
        log(TRACE, marker, format, args);
    }

    @Override
    public void trace(final Marker marker, final String msg, final Throwable throwable) {
        log(TRACE, marker, msg, throwable);
    }

    /**
     * @return whether this logger is debug enabled in this thread
     */
    @Override
    public boolean isDebugEnabled() {
        return enabledLevels.get().contains(DEBUG);
    }

    @Override
    public void debug(final String message) {
        log(DEBUG, message);
    }

    @Override
    public void debug(final String format, final Object arg) {
        log(DEBUG, format, arg);
    }

    @Override
    public void debug(final String format, final Object arg1, final Object arg2) {
        log(DEBUG, format, arg1, arg2);
    }

    @Override
    public void debug(final String format, final Object... args) {
        log(DEBUG, format, args);
    }

    @Override
    public void debug(final String msg, final Throwable throwable) {
        log(DEBUG, msg, throwable);
    }

    @Override
    public boolean isDebugEnabled(final Marker marker) {
        return enabledLevels.get().contains(DEBUG);
    }

    @Override
    public void debug(final Marker marker, final String msg) {
        log(DEBUG, marker, msg);
    }

    @Override
    public void debug(final Marker marker, final String format, final Object arg) {
        log(DEBUG, marker, format, arg);
    }

    @Override
    public void debug(final Marker marker, final String format, final Object arg1, final Object arg2) {
        log(DEBUG, marker, format, arg1, arg2);
    }

    @Override
    public void debug(final Marker marker, final String format, final Object... args) {
        log(DEBUG, marker, format, args);
    }

    @Override
    public void debug(final Marker marker, final String msg, final Throwable throwable) {
        log(DEBUG, marker, msg, throwable);
    }

    /**
     * @return whether this logger is info enabled in this thread
     */
    @Override
    public boolean isInfoEnabled() {
        return enabledLevels.get().contains(INFO);
    }

    @Override
    public void info(final String message) {
        log(INFO, message);
    }

    @Override
    public void info(final String format, final Object arg) {
        log(INFO, format, arg);
    }

    @Override
    public void info(final String format, final Object arg1, final Object arg2) {
        log(INFO, format, arg1, arg2);
    }

    @Override
    public void info(final String format, final Object... args) {
        log(INFO, format, args);
    }

    @Override
    public void info(final String msg, final Throwable throwable) {
        log(INFO, msg, throwable);
    }

    @Override
    public boolean isInfoEnabled(final Marker marker) {
        return enabledLevels.get().contains(INFO);
    }

    @Override
    public void info(final Marker marker, final String msg) {
        log(INFO, marker, msg);
    }

    @Override
    public void info(final Marker marker, final String format, final Object arg) {
        log(INFO, marker, format, arg);
    }

    @Override
    public void info(final Marker marker, final String format, final Object arg1, final Object arg2) {
        log(INFO, marker, format, arg1, arg2);
    }

    @Override
    public void info(final Marker marker, final String format, final Object... args) {
        log(INFO, marker, format, args);
    }

    @Override
    public void info(final Marker marker, final String msg, final Throwable throwable) {
        log(INFO, marker, msg, throwable);
    }

    /**
     * @return whether this logger is warn enabled in this thread
     */
    @Override
    public boolean isWarnEnabled() {
        return enabledLevels.get().contains(WARN);
    }

    @Override
    public void warn(final String message) {
        log(WARN, message);
    }

    @Override
    public void warn(final String format, final Object arg) {
        log(WARN, format, arg);
    }

    @Override
    public void warn(final String format, final Object arg1, final Object arg2) {
        log(WARN, format, arg1, arg2);
    }

    @Override
    public void warn(final String format, final Object... args) {
        log(WARN, format, args);
    }

    @Override
    public void warn(final String msg, final Throwable throwable) {
        log(WARN, msg, throwable);
    }

    @Override
    public boolean isWarnEnabled(final Marker marker) {
        return enabledLevels.get().contains(WARN);
    }

    @Override
    public void warn(final Marker marker, final String msg) {
        log(WARN, marker, msg);
    }

    @Override
    public void warn(final Marker marker, final String format, final Object arg) {
        log(WARN, marker, format, arg);
    }

    @Override
    public void warn(final Marker marker, final String format, final Object arg1, final Object arg2) {
        log(WARN, marker, format, arg1, arg2);
    }

    @Override
    public void warn(final Marker marker, final String format, final Object... args) {
        log(WARN, marker, format, args);
    }

    @Override
    public void warn(final Marker marker, final String msg, final Throwable throwable) {
        log(WARN, marker, msg, throwable);
    }

    /**
     * @return whether this logger is error enabled in this thread
     */
    @Override
    public boolean isErrorEnabled() {
        return enabledLevels.get().contains(ERROR);
    }

    @Override
    public void error(final String message) {
        log(ERROR, message);
    }

    @Override
    public void error(final String format, final Object arg) {
        log(ERROR, format, arg);
    }

    @Override
    public void error(final String format, final Object arg1, final Object arg2) {
        log(ERROR, format, arg1, arg2);
    }

    @Override
    public void error(final String format, final Object... args) {
        log(ERROR, format, args);
    }

    @Override
    public void error(final String msg, final Throwable throwable) {
        log(ERROR, msg, throwable);
    }

    @Override
    public boolean isErrorEnabled(final Marker marker) {
        return enabledLevels.get().contains(ERROR);
    }

    @Override
    public void error(final Marker marker, final String msg) {
        log(ERROR, marker, msg);
    }

    @Override
    public void error(final Marker marker, final String format, final Object arg) {
        log(ERROR, marker, format, arg);
    }

    @Override
    public void error(final Marker marker, final String format, final Object arg1, final Object arg2) {
        log(ERROR, marker, format, arg1, arg2);
    }

    @Override
    public void error(final Marker marker, final String format, final Object... args) {
        log(ERROR, marker, format, args);
    }

    @Override
    public void error(final Marker marker, final String msg, final Throwable throwable) {
        log(ERROR, marker, msg, throwable);
    }

    private void log(final Level level, final String format, final Object... args) {
        log(level, format, Optional.<Marker>absent(), args);
    }

    private void log(final Level level, final String msg, final Throwable throwable) { //NOPMD PMD wrongly thinks unused...
        addLoggingEvent(level, Optional.<Marker>absent(), fromNullable(throwable), msg);
    }

    private void log(final Level level, final Marker marker, final String format, final Object... args) {
        log(level, format, fromNullable(marker), args);
    }

    private void log(final Level level, final Marker marker, final String msg, final Throwable throwable) {
        addLoggingEvent(level, fromNullable(marker), fromNullable(throwable), msg);
    }

    private void log(final Level level, final String format, final Optional<Marker> marker, final Object[] args) {
        final FormattingTuple formattedArgs = MessageFormatter.arrayFormat(format, args);
        addLoggingEvent(level, marker, fromNullable(formattedArgs.getThrowable()), format, formattedArgs.getArgArray());
    }

    private void addLoggingEvent(
            final Level level,
            final Optional<Marker> marker,
            final Optional<Throwable> throwable,
            final String format,
            final Object... args) {
        if (enabledLevels.get().contains(level)) {
            final LoggingEvent event = new LoggingEvent(of(this), level, mdc(), marker, throwable, format, args);
            allLoggingEvents.add(event);
            loggingEvents.get().add(event);
            testLoggerFactory.addLoggingEvent(event);
            optionallyPrint(event);
        }
    }

    @SuppressWarnings("unchecked")
    private Map<String, String> mdc() {
        return fromNullable(MDC.getCopyOfContextMap()).or(Collections.emptyMap());
    }

    private void optionallyPrint(final LoggingEvent event) {
        if (testLoggerFactory.getPrintLevel().compareTo(event.getLevel()) <= 0) {
            event.print();
        }
    }

    /**
     * @return the set of levels enabled for this logger on this thread
     */
    public ImmutableSet<Level> getEnabledLevels() {
        return enabledLevels.get();
    }

    /**
     * The conventional hierarchical notion of Levels, where info being enabled implies warn and error being enabled, is not a
     * requirement of the SLF4J API, so all levels you wish to enable must be passed explicitly to this method.  If you wish to
     * use traditional hierarchical setups you may conveniently do so by using the constants in
     * {@link uk.org.lidalia.slf4jext.ConventionalLevelHierarchy}
     *
     * @param enabledLevels levels which will be considered enabled for this logger IN THIS THREAD;
     *                      does not affect enabled levels for this logger in other threads
     */
    public void setEnabledLevels(final ImmutableSet<Level> enabledLevels) {
        this.enabledLevels.set(enabledLevels);
    }

    /**
     * The conventional hierarchical notion of Levels, where info being enabled implies warn and error being enabled, is not a
     * requirement of the SLF4J API, so all levels you wish to enable must be passed explicitly to this method.  If you wish to
     * use traditional hierarchical setups you may conveniently do so by passing the constants in
     * {@link uk.org.lidalia.slf4jext.ConventionalLevelHierarchy} to {@link #setEnabledLevels(ImmutableSet)}
     *
     * @param enabledLevels levels which will be considered enabled for this logger IN THIS THREAD;
     *                      does not affect enabled levels for this logger in other threads
     */
    public void setEnabledLevels(final Level... enabledLevels) {
        setEnabledLevels(immutableEnumSet(asList(enabledLevels)));
    }

    /**
     * The conventional hierarchical notion of Levels, where info being enabled implies warn and error being enabled, is not a
     * requirement of the SLF4J API, so all levels you wish to enable must be passed explicitly to this method.  If you wish to
     * use traditional hierarchical setups you may conveniently do so by using the constants in
     * {@link uk.org.lidalia.slf4jext.ConventionalLevelHierarchy}
     *
     * @param enabledLevelsForAllThreads levels which will be considered enabled for this logger IN ALL THREADS
     */
    public void setEnabledLevelsForAllThreads(final ImmutableSet<Level> enabledLevelsForAllThreads) {
        this.enabledLevels = new ThreadLocal<>(enabledLevelsForAllThreads);
    }

    /**
     * The conventional hierarchical notion of Levels, where info being enabled implies warn and error being enabled, is not a
     * requirement of the SLF4J API, so all levels you wish to enable must be passed explicitly to this method.  If you wish to
     * use traditional hierarchical setups you may conveniently do so by passing the constants in
     * {@link uk.org.lidalia.slf4jext.ConventionalLevelHierarchy} to {@link #setEnabledLevelsForAllThreads(ImmutableSet)}
     *
     * @param enabledLevelsForAllThreads levels which will be considered enabled for this logger IN ALL THREADS
     */
    public void setEnabledLevelsForAllThreads(final Level... enabledLevelsForAllThreads) {
        setEnabledLevelsForAllThreads(ImmutableSet.copyOf(enabledLevelsForAllThreads));
    }
}