001package jmri.util;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.util.*;
006
007import javax.annotation.Nonnull;
008
009import org.slf4j.Logger;
010
011/**
012 * Basic utilities for logging special messages.
013 *
014 * @author Randall Wood Copyright 2020
015 */
016public class LoggingUtil {
017
018    protected static Map<Logger, Set<String>> warnedOnce = new HashMap<>();
019    protected static Map<Logger, Set<String>> infodOnce = new HashMap<>();
020    protected static boolean logDeprecations = true;
021
022    /**
023     * Emit a particular WARNING-level message just once.
024     * <p>
025     * Goal is to be lightweight and fast; this will only be used in a few
026     * places, and only those should appear in data structure.
027     *
028     * @param logger the source of the warning
029     * @param msg    warning message
030     * @param args   message arguments
031     * @return true if the log was emitted this time
032     */
033    @SuppressFBWarnings( value = {"SLF4J_UNKNOWN_ARRAY", "SLF4J_FORMAT_SHOULD_BE_CONST"},
034        justification = "Passing arguments through")
035    public static boolean warnOnce(@Nonnull Logger logger, @Nonnull String msg, Object... args) {
036        Set<String> loggerSet = warnedOnce.computeIfAbsent(logger, l -> new HashSet<>());
037        // if it exists, there was a prior warning given
038        if (loggerSet.contains(msg)) {
039            return false;
040        }
041        loggerSet.add(msg);
042        logger.warn(msg, args);
043        return true;
044    }
045
046    /**
047     * Emit a particular INFO-level message just once.
048     * <p>
049     * Goal is to be lightweight and fast; this will only be used in a few
050     * places, and only those should appear in data structure.
051     *
052     * @param logger the source of the warning
053     * @param msg    info message
054     * @param args   message arguments
055     * @return true if the log was emitted this time
056     */
057    @SuppressFBWarnings( value = {"SLF4J_UNKNOWN_ARRAY","SLF4J_FORMAT_SHOULD_BE_CONST"},
058        justification = "Passing arguments through")
059    public static boolean infoOnce(@Nonnull Logger logger, @Nonnull String msg, Object... args) {
060        Set<String> loggerSet = infodOnce.computeIfAbsent(logger, l -> new HashSet<>());
061        // if it exists, there was a prior info given
062        if (loggerSet.contains(msg)) {
063            return false;
064        }
065        loggerSet.add(msg);
066        logger.info(msg, args);
067        return true;
068    }
069
070    /**
071     * Warn that a deprecated method has been invoked.
072     * <p>
073     * Can also be used to warn of some deprecated condition, i.e.
074     * obsolete-format input data.
075     * <p>
076     * The logging is turned off by default during testing to simplify updating
077     * tests when warnings are added.
078     *
079     * @param logger     The Logger to warn.
080     * @param methodName method name.
081     */
082    public static void deprecationWarning(@Nonnull Logger logger, @Nonnull String methodName) {
083        if (logDeprecations) {
084            warnOnce(logger, "{} is deprecated, please remove references to it", methodName, shortenStacktrace(new Exception("traceback")));
085        }
086    }
087
088    /**
089     * Shorten a stack trace to start with the first JMRI method.
090     * <p>
091     * When logged, the stack trace will be more focused.
092     *
093     * @param <T> the type of Throwable
094     * @param t   the Throwable containing the stack trace to truncate
095     * @return t with truncated stack trace
096     */
097    @Nonnull
098    public static <T extends Throwable> T shortenStacktrace(@Nonnull T t) {
099        StackTraceElement[] originalTrace = t.getStackTrace();
100        int i;
101        for (i = originalTrace.length - 1; i > 0; i--) {
102            // search from deepest
103            String name = originalTrace[i].getClassName();
104            if (name.equals("jmri.util.junit.TestClassMainMethod")) {
105                continue; // special case to ignore high up in stack
106            }
107            if (name.equals("apps.tests.AllTest")) {
108                continue; // special case to ignore high up in stack
109            }
110            if (name.equals("jmri.HeadLessTest")) {
111                continue; // special case to ignore high up in stack
112            }
113            if (name.startsWith("jmri") || name.startsWith("apps")) {
114                break; // keep those
115            }
116        }
117        return shortenStacktrace(t, i + 1);
118    }
119
120    /**
121     * Shorten a stack trace to a fixed length.
122     * <p>
123     * When logged, the stack trace will be more focused.
124     *
125     * @param <T> the type of Throwable
126     * @param t   the Throwable containing the stack trace to truncate
127     * @param len length of stack trace to retain
128     * @return t with truncated stack trace
129     */
130    @Nonnull
131    public static <T extends Throwable> T shortenStacktrace(@Nonnull T t, int len) {
132        StackTraceElement[] originalTrace = t.getStackTrace();
133        int newLen = Math.min(len, originalTrace.length);
134        StackTraceElement[] newTrace = new StackTraceElement[newLen];
135        System.arraycopy(originalTrace, 0, newTrace, 0, newLen);
136        t.setStackTrace(newTrace);
137        return t;
138    }
139
140}