001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.OutputStream;
007import java.io.PrintWriter;
008import java.io.StringWriter;
009import java.text.MessageFormat;
010import java.util.ArrayList;
011import java.util.Arrays;
012import java.util.List;
013import java.util.function.Supplier;
014import java.util.logging.ConsoleHandler;
015import java.util.logging.Handler;
016import java.util.logging.Level;
017import java.util.logging.LogRecord;
018import java.util.logging.Logger;
019
020import org.openstreetmap.josm.tools.bugreport.BugReport;
021
022/**
023 * This class contains utility methods to log errors and warnings.
024 * <p>
025 * There are multiple log levels supported.
026 * @author Michael Zangl
027 * @since 10899
028 */
029public final class Logging {
030    /**
031     * The josm internal log level indicating a severe error in the application that usually leads to a crash.
032     */
033    public static final Level LEVEL_ERROR = Level.SEVERE;
034    /**
035     * The josm internal log level to use when something that may lead to a crash or wrong behaviour has happened.
036     */
037    public static final Level LEVEL_WARN = Level.WARNING;
038    /**
039     * The josm internal log level to use for important events that will be useful when debugging problems
040     */
041    public static final Level LEVEL_INFO = Level.INFO;
042    /**
043     * The josm internal log level to print debug output
044     */
045    public static final Level LEVEL_DEBUG = Level.FINE;
046    /**
047     * The finest log level josm supports. This lets josm print a lot of debug output.
048     */
049    public static final Level LEVEL_TRACE = Level.FINEST;
050    private static final Logger LOGGER = Logger.getAnonymousLogger();
051    private static final RememberWarningHandler WARNINGS = new RememberWarningHandler();
052
053    /**
054     * A {@link ConsoleHandler} with a couple of extra features, allowing it to be targeted at an
055     * an arbitrary {@link OutputStream} which it can be asked to reaquire the reference for on demand
056     * through {@link #reacquireOutputStream()}. It can also prevent a LogRecord's output if a
057     * specified {@code prioritizedHandler} would have outputted it.
058     * @since 14052
059     */
060    public static class ReacquiringConsoleHandler extends ConsoleHandler {
061        private final Supplier<OutputStream> outputStreamSupplier;
062        private final Handler prioritizedHandler;
063        private OutputStream outputStreamMemo;
064
065        /**
066        * Construct a new {@link ReacquiringConsoleHandler}.
067        * @param outputStreamSupplier A {@link Supplier} which will return the desired
068        *   {@link OutputStream} for this handler when called. Particularly useful if you happen to be
069        *   using a test framework which will switch out references to the stderr/stdout streams with
070        *   new dummy ones from time to time.
071        * @param prioritizedHandler If non-null, will suppress output of any log records which pass this
072        *   handler's {@code Handler#isLoggable(LogRecord)} method.
073        */
074        public ReacquiringConsoleHandler(
075            final Supplier<OutputStream> outputStreamSupplier,
076            final Handler prioritizedHandler
077        ) {
078            this.outputStreamSupplier = outputStreamSupplier;
079            this.prioritizedHandler = prioritizedHandler;
080
081            this.reacquireOutputStream();
082        }
083
084        /**
085         * Set output stream to one acquired from calling outputStreamSupplier
086         */
087        public synchronized void reacquireOutputStream() {
088            final OutputStream reacquiredStream = this.outputStreamSupplier.get();
089
090            // only bother calling setOutputStream if it's actually different, as setOutputStream
091            // has the nasty side effect of closing any previous output stream, which is certainly not
092            // what we would want were the new stream the same one
093            if (reacquiredStream != this.outputStreamMemo) {
094                this.setOutputStream(reacquiredStream);
095            }
096        }
097
098        @Override
099        public synchronized void setOutputStream(final OutputStream outputStream) {
100            // this wouldn't be necessary if StreamHandler made it possible to see what the current
101            // output stream is set to
102            this.outputStreamMemo = outputStream;
103            super.setOutputStream(outputStream);
104        }
105
106        @Override
107        public synchronized void publish(LogRecord record) {
108            if (this.prioritizedHandler == null || !this.prioritizedHandler.isLoggable(record)) {
109                super.publish(record);
110            }
111        }
112    }
113
114    static {
115        // We need to be sure java.locale.providers system property is initialized by JOSM, not by JRE
116        // The call to ConsoleHandler constructor makes the JRE access this property by side effect
117        I18n.setupJavaLocaleProviders();
118
119        LOGGER.setLevel(Level.ALL);
120        LOGGER.setUseParentHandlers(false);
121
122        // for a more concise logging output via java.util.logging.SimpleFormatter
123        Utils.updateSystemProperty("java.util.logging.SimpleFormatter.format", "%1$tF %1$tT.%1$tL %4$s: %5$s%6$s%n");
124
125        ConsoleHandler stderr = new ReacquiringConsoleHandler(() -> System.err, null);
126        LOGGER.addHandler(stderr);
127        try {
128            stderr.setLevel(LEVEL_WARN);
129        } catch (SecurityException e) {
130            System.err.println("Unable to set logging level: " + e.getMessage());
131        }
132
133        ConsoleHandler stdout = new ReacquiringConsoleHandler(() -> System.out, stderr);
134        LOGGER.addHandler(stdout);
135        try {
136            stdout.setLevel(Level.ALL);
137        } catch (SecurityException e) {
138            System.err.println("Unable to set logging level: " + e.getMessage());
139        }
140
141        LOGGER.addHandler(WARNINGS);
142        // Set log level to info, otherwise the first ListenerList created will be for debugging purposes and create memory leaks
143        Logging.setLogLevel(Logging.LEVEL_INFO);
144    }
145
146    private Logging() {
147        // hide
148    }
149
150    /**
151     * Set the global log level.
152     * @param level The log level to use
153     */
154    public static void setLogLevel(Level level) {
155        LOGGER.setLevel(level);
156    }
157
158    /**
159     * Prints an error message if logging is on.
160     * @param message The message to print.
161     */
162    public static void error(String message) {
163        logPrivate(LEVEL_ERROR, message);
164    }
165
166    /**
167     * Prints a formatted error message if logging is on. Calls {@link MessageFormat#format}
168     * function to format text.
169     * @param pattern The formatted message to print.
170     * @param args The objects to insert into format string.
171     */
172    public static void error(String pattern, Object... args) {
173        logPrivate(LEVEL_ERROR, pattern, args);
174    }
175
176    /**
177     * Prints an error message for the given Throwable if logging is on.
178     * @param t The throwable object causing the error.
179     * @since 12620
180     */
181    public static void error(Throwable t) {
182        logWithStackTrace(Logging.LEVEL_ERROR, t);
183    }
184
185    /**
186     * Prints a warning message if logging is on.
187     * @param message The message to print.
188     */
189    public static void warn(String message) {
190        logPrivate(LEVEL_WARN, message);
191    }
192
193    /**
194     * Prints a formatted warning message if logging is on. Calls {@link MessageFormat#format}
195     * function to format text.
196     * @param pattern The formatted message to print.
197     * @param args The objects to insert into format string.
198     */
199    public static void warn(String pattern, Object... args) {
200        logPrivate(LEVEL_WARN, pattern, args);
201    }
202
203    /**
204     * Prints a warning message for the given Throwable if logging is on.
205     * @param t The throwable object causing the error.
206     * @since 12620
207     */
208    public static void warn(Throwable t) {
209        logWithStackTrace(Logging.LEVEL_WARN, t);
210    }
211
212    /**
213     * Prints a info message if logging is on.
214     * @param message The message to print.
215     */
216    public static void info(String message) {
217        logPrivate(LEVEL_INFO, message);
218    }
219
220    /**
221     * Prints a formatted info message if logging is on. Calls {@link MessageFormat#format}
222     * function to format text.
223     * @param pattern The formatted message to print.
224     * @param args The objects to insert into format string.
225     */
226    public static void info(String pattern, Object... args) {
227        logPrivate(LEVEL_INFO, pattern, args);
228    }
229
230    /**
231     * Prints a info message for the given Throwable if logging is on.
232     * @param t The throwable object causing the error.
233     * @since 12620
234     */
235    public static void info(Throwable t) {
236        logWithStackTrace(Logging.LEVEL_INFO, t);
237    }
238
239    /**
240     * Prints a debug message if logging is on.
241     * @param message The message to print.
242     */
243    public static void debug(String message) {
244        logPrivate(LEVEL_DEBUG, message);
245    }
246
247    /**
248     * Prints a formatted debug message if logging is on. Calls {@link MessageFormat#format}
249     * function to format text.
250     * @param pattern The formatted message to print.
251     * @param args The objects to insert into format string.
252     */
253    public static void debug(String pattern, Object... args) {
254        logPrivate(LEVEL_DEBUG, pattern, args);
255    }
256
257    /**
258     * Prints a debug message for the given Throwable if logging is on.
259     * @param t The throwable object causing the error.
260     * @since 12620
261     */
262    public static void debug(Throwable t) {
263        log(Logging.LEVEL_DEBUG, t);
264    }
265
266    /**
267     * Prints a trace message if logging is on.
268     * @param message The message to print.
269     */
270    public static void trace(String message) {
271        logPrivate(LEVEL_TRACE, message);
272    }
273
274    /**
275     * Prints a formatted trace message if logging is on. Calls {@link MessageFormat#format}
276     * function to format text.
277     * @param pattern The formatted message to print.
278     * @param args The objects to insert into format string.
279     */
280    public static void trace(String pattern, Object... args) {
281        logPrivate(LEVEL_TRACE, pattern, args);
282    }
283
284    /**
285     * Prints a trace message for the given Throwable if logging is on.
286     * @param t The throwable object causing the error.
287     * @since 12620
288     */
289    public static void trace(Throwable t) {
290        log(Logging.LEVEL_TRACE, t);
291    }
292
293    /**
294     * Logs a throwable that happened. The stack trace is not added to the log.
295     * @param level The level.
296     * @param t The throwable that should be logged.
297     * @see #logWithStackTrace(Level, Throwable)
298     */
299    public static void log(Level level, Throwable t) {
300        logPrivate(level, () -> getErrorLog(null, t));
301    }
302
303    /**
304     * Logs a throwable that happened. The stack trace is not added to the log.
305     * @param level The level.
306     * @param message An additional error message
307     * @param t The throwable that caused the message
308     * @see #logWithStackTrace(Level, String, Throwable)
309     */
310    public static void log(Level level, String message, Throwable t) {
311        logPrivate(level, () -> getErrorLog(message, t));
312    }
313
314    /**
315     * Logs a throwable that happened. Adds the stack trace to the log.
316     * @param level The level.
317     * @param t The throwable that should be logged.
318     * @see #log(Level, Throwable)
319     */
320    public static void logWithStackTrace(Level level, Throwable t) {
321        logPrivate(level, () -> getErrorLogWithStack(null, t));
322    }
323
324    /**
325     * Logs a throwable that happened. Adds the stack trace to the log.
326     * @param level The level.
327     * @param message An additional error message
328     * @param t The throwable that should be logged.
329     * @see #logWithStackTrace(Level, Throwable)
330     */
331    public static void logWithStackTrace(Level level, String message, Throwable t) {
332        logPrivate(level, () -> getErrorLogWithStack(message, t));
333    }
334
335    /**
336     * Logs a throwable that happened. Adds the stack trace to the log.
337     * @param level The level.
338     * @param t The throwable that should be logged.
339     * @param pattern The formatted message to print.
340     * @param args The objects to insert into format string
341     * @see #logWithStackTrace(Level, Throwable)
342     */
343    public static void logWithStackTrace(Level level, Throwable t, String pattern, Object... args) {
344        logPrivate(level, () -> getErrorLogWithStack(MessageFormat.format(pattern, args), t));
345    }
346
347    private static void logPrivate(Level level, String pattern, Object... args) {
348        logPrivate(level, () -> MessageFormat.format(pattern, args));
349    }
350
351    private static void logPrivate(Level level, String message) {
352        logPrivate(level, () -> message);
353    }
354
355    private static void logPrivate(Level level, Supplier<String> supplier) {
356        // all log methods immediately call one of the logPrivate methods.
357        if (LOGGER.isLoggable(level)) {
358            StackTraceElement callingMethod = BugReport.getCallingMethod(1, Logging.class.getName(), name -> !"logPrivate".equals(name));
359            LOGGER.logp(level, callingMethod.getClassName(), callingMethod.getMethodName(), supplier);
360        }
361    }
362
363    /**
364     * Tests if a given log level is enabled. This can be used to avoid constructing debug data if required.
365     *
366     * For formatting text, you should use the {@link #debug(String, Object...)} message
367     * @param level A level constant. You can e.g. use {@link Logging#LEVEL_ERROR}
368     * @return <code>true</code> if log level is enabled.
369     */
370    public static boolean isLoggingEnabled(Level level) {
371        return LOGGER.isLoggable(level);
372    }
373
374    /**
375     * Determines if debug log level is enabled.
376     * Useful to avoid costly construction of debug messages when not enabled.
377     * @return {@code true} if log level is at least debug, {@code false} otherwise
378     * @since 12620
379     */
380    public static boolean isDebugEnabled() {
381        return isLoggingEnabled(Logging.LEVEL_DEBUG);
382    }
383
384    /**
385     * Determines if trace log level is enabled.
386     * Useful to avoid costly construction of trace messages when not enabled.
387     * @return {@code true} if log level is at least trace, {@code false} otherwise
388     * @since 12620
389     */
390    public static boolean isTraceEnabled() {
391        return isLoggingEnabled(Logging.LEVEL_TRACE);
392    }
393
394    private static String getErrorLog(String message, Throwable t) {
395        StringBuilder sb = new StringBuilder();
396        if (message != null) {
397            sb.append(message).append(": ");
398        }
399        sb.append(getErrorMessage(t));
400        return sb.toString();
401    }
402
403    private static String getErrorLogWithStack(String message, Throwable t) {
404        StringWriter sb = new StringWriter();
405        sb.append(getErrorLog(message, t));
406        if (t != null) {
407            sb.append('\n');
408            t.printStackTrace(new PrintWriter(sb));
409        }
410        return sb.toString();
411    }
412
413    /**
414     * Returns a human-readable message of error, also usable for developers.
415     * @param t The error
416     * @return The human-readable error message
417     */
418    public static String getErrorMessage(Throwable t) {
419        if (t == null) {
420            return "(no error)";
421        }
422        StringBuilder sb = new StringBuilder(t.getClass().getName());
423        String msg = t.getMessage();
424        if (msg != null) {
425            sb.append(": ").append(msg.trim());
426        }
427        Throwable cause = t.getCause();
428        if (cause != null && !cause.equals(t)) {
429            // this may cause infinite loops in the unlikely case that there is a loop in the causes.
430            sb.append(". ").append(tr("Cause: ")).append(getErrorMessage(cause));
431        }
432        return sb.toString();
433    }
434
435    /**
436     * Clear the list of last warnings
437     */
438    public static void clearLastErrorAndWarnings() {
439        WARNINGS.clear();
440    }
441
442    /**
443     * Get the last error and warning messages in the order in which they were received.
444     * @return The last errors and warnings.
445     */
446    public static List<String> getLastErrorAndWarnings() {
447        return WARNINGS.getMessages();
448    }
449
450    /**
451     * Provides direct access to the logger used. Use of methods like {@link #warn(String)} is prefered.
452     * @return The logger
453     */
454    public static Logger getLogger() {
455        return LOGGER;
456    }
457
458    private static class RememberWarningHandler extends Handler {
459        private final String[] log = new String[10];
460        private int messagesLogged;
461
462        synchronized void clear() {
463            messagesLogged = 0;
464            Arrays.fill(log, null);
465        }
466
467        @Override
468        public synchronized void publish(LogRecord record) {
469            // We don't use setLevel + isLoggable to work in WebStart Sandbox mode
470            if (record.getLevel().intValue() < LEVEL_WARN.intValue()) {
471                return;
472            }
473
474            String msg = getPrefix(record) + record.getMessage();
475
476            // Only remember first line of message
477            int idx = msg.indexOf('\n');
478            if (idx > 0) {
479                msg = msg.substring(0, idx);
480            }
481            log[messagesLogged % log.length] = msg;
482            messagesLogged++;
483        }
484
485        private static String getPrefix(LogRecord record) {
486            if (record.getLevel().equals(LEVEL_WARN)) {
487                return "W: ";
488            } else {
489                // worse than warn
490                return "E: ";
491            }
492        }
493
494        synchronized List<String> getMessages() {
495            List<String> logged = Arrays.asList(log);
496            ArrayList<String> res = new ArrayList<>();
497            int logOffset = messagesLogged % log.length;
498            if (messagesLogged > logOffset) {
499                res.addAll(logged.subList(logOffset, log.length));
500            }
501            res.addAll(logged.subList(0, logOffset));
502            return res;
503        }
504
505        @Override
506        public synchronized void flush() {
507            // nothing to do
508        }
509
510        @Override
511        public void close() {
512            // nothing to do
513        }
514    }
515}