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    static {
054        // We need to be sure java.locale.providers system property is initialized by JOSM, not by JRE
055        // The call to ConsoleHandler constructor makes the JRE access this property by side effect
056        I18n.setupJavaLocaleProviders();
057
058        LOGGER.setLevel(Level.ALL);
059        LOGGER.setUseParentHandlers(false);
060
061        // for a more concise logging output via java.util.logging.SimpleFormatter
062        Utils.updateSystemProperty("java.util.logging.SimpleFormatter.format", "%1$tF %1$tT.%1$tL %4$s: %5$s%6$s%n");
063
064        ConsoleHandler stderr = new ConsoleHandler();
065        LOGGER.addHandler(stderr);
066        try {
067            stderr.setLevel(LEVEL_WARN);
068        } catch (SecurityException e) {
069            System.err.println("Unable to set logging level: " + e.getMessage());
070        }
071
072        ConsoleHandler stdout = new ConsoleHandler() {
073            @Override
074            protected synchronized void setOutputStream(OutputStream out) {
075                // overwrite output stream.
076                super.setOutputStream(System.out);
077            }
078
079            @Override
080            public synchronized void publish(LogRecord record) {
081                if (!stderr.isLoggable(record)) {
082                    super.publish(record);
083                }
084            }
085        };
086        LOGGER.addHandler(stdout);
087        try {
088            stdout.setLevel(Level.ALL);
089        } catch (SecurityException e) {
090            System.err.println("Unable to set logging level: " + e.getMessage());
091        }
092
093        LOGGER.addHandler(WARNINGS);
094        // Set log level to info, otherwise the first ListenerList created will be for debugging purposes and create memory leaks
095        Logging.setLogLevel(Logging.LEVEL_INFO);
096    }
097
098    private Logging() {
099        // hide
100    }
101
102    /**
103     * Set the global log level.
104     * @param level The log level to use
105     */
106    public static void setLogLevel(Level level) {
107        LOGGER.setLevel(level);
108    }
109
110    /**
111     * Prints an error message if logging is on.
112     * @param message The message to print.
113     */
114    public static void error(String message) {
115        logPrivate(LEVEL_ERROR, message);
116    }
117
118    /**
119     * Prints a formatted error message if logging is on. Calls {@link MessageFormat#format}
120     * function to format text.
121     * @param pattern The formatted message to print.
122     * @param args The objects to insert into format string.
123     */
124    public static void error(String pattern, Object... args) {
125        logPrivate(LEVEL_ERROR, pattern, args);
126    }
127
128    /**
129     * Prints an error message for the given Throwable if logging is on.
130     * @param t The throwable object causing the error.
131     * @since 12620
132     */
133    public static void error(Throwable t) {
134        logWithStackTrace(Logging.LEVEL_ERROR, t);
135    }
136
137    /**
138     * Prints a warning message if logging is on.
139     * @param message The message to print.
140     */
141    public static void warn(String message) {
142        logPrivate(LEVEL_WARN, message);
143    }
144
145    /**
146     * Prints a formatted warning message if logging is on. Calls {@link MessageFormat#format}
147     * function to format text.
148     * @param pattern The formatted message to print.
149     * @param args The objects to insert into format string.
150     */
151    public static void warn(String pattern, Object... args) {
152        logPrivate(LEVEL_WARN, pattern, args);
153    }
154
155    /**
156     * Prints a warning message for the given Throwable if logging is on.
157     * @param t The throwable object causing the error.
158     * @since 12620
159     */
160    public static void warn(Throwable t) {
161        logWithStackTrace(Logging.LEVEL_WARN, t);
162    }
163
164    /**
165     * Prints a info message if logging is on.
166     * @param message The message to print.
167     */
168    public static void info(String message) {
169        logPrivate(LEVEL_INFO, message);
170    }
171
172    /**
173     * Prints a formatted info message if logging is on. Calls {@link MessageFormat#format}
174     * function to format text.
175     * @param pattern The formatted message to print.
176     * @param args The objects to insert into format string.
177     */
178    public static void info(String pattern, Object... args) {
179        logPrivate(LEVEL_INFO, pattern, args);
180    }
181
182    /**
183     * Prints a info message for the given Throwable if logging is on.
184     * @param t The throwable object causing the error.
185     * @since 12620
186     */
187    public static void info(Throwable t) {
188        logWithStackTrace(Logging.LEVEL_INFO, t);
189    }
190
191    /**
192     * Prints a debug message if logging is on.
193     * @param message The message to print.
194     */
195    public static void debug(String message) {
196        logPrivate(LEVEL_DEBUG, message);
197    }
198
199    /**
200     * Prints a formatted debug message if logging is on. Calls {@link MessageFormat#format}
201     * function to format text.
202     * @param pattern The formatted message to print.
203     * @param args The objects to insert into format string.
204     */
205    public static void debug(String pattern, Object... args) {
206        logPrivate(LEVEL_DEBUG, pattern, args);
207    }
208
209    /**
210     * Prints a debug message for the given Throwable if logging is on.
211     * @param t The throwable object causing the error.
212     * @since 12620
213     */
214    public static void debug(Throwable t) {
215        log(Logging.LEVEL_DEBUG, t);
216    }
217
218    /**
219     * Prints a trace message if logging is on.
220     * @param message The message to print.
221     */
222    public static void trace(String message) {
223        logPrivate(LEVEL_TRACE, message);
224    }
225
226    /**
227     * Prints a formatted trace message if logging is on. Calls {@link MessageFormat#format}
228     * function to format text.
229     * @param pattern The formatted message to print.
230     * @param args The objects to insert into format string.
231     */
232    public static void trace(String pattern, Object... args) {
233        logPrivate(LEVEL_TRACE, pattern, args);
234    }
235
236    /**
237     * Prints a trace message for the given Throwable if logging is on.
238     * @param t The throwable object causing the error.
239     * @since 12620
240     */
241    public static void trace(Throwable t) {
242        log(Logging.LEVEL_TRACE, t);
243    }
244
245    /**
246     * Logs a throwable that happened. The stack trace is not added to the log.
247     * @param level The level.
248     * @param t The throwable that should be logged.
249     * @see #logWithStackTrace(Level, Throwable)
250     */
251    public static void log(Level level, Throwable t) {
252        logPrivate(level, () -> getErrorLog(null, t));
253    }
254
255    /**
256     * Logs a throwable that happened. The stack trace is not added to the log.
257     * @param level The level.
258     * @param message An additional error message
259     * @param t The throwable that caused the message
260     * @see #logWithStackTrace(Level, String, Throwable)
261     */
262    public static void log(Level level, String message, Throwable t) {
263        logPrivate(level, () -> getErrorLog(message, t));
264    }
265
266    /**
267     * Logs a throwable that happened. Adds the stack trace to the log.
268     * @param level The level.
269     * @param t The throwable that should be logged.
270     * @see #log(Level, Throwable)
271     */
272    public static void logWithStackTrace(Level level, Throwable t) {
273        logPrivate(level, () -> getErrorLogWithStack(null, t));
274    }
275
276    /**
277     * Logs a throwable that happened. Adds the stack trace to the log.
278     * @param level The level.
279     * @param message An additional error message
280     * @param t The throwable that should be logged.
281     * @see #logWithStackTrace(Level, Throwable)
282     */
283    public static void logWithStackTrace(Level level, String message, Throwable t) {
284        logPrivate(level, () -> getErrorLogWithStack(message, t));
285    }
286
287    /**
288     * Logs a throwable that happened. Adds the stack trace to the log.
289     * @param level The level.
290     * @param t The throwable that should be logged.
291     * @param pattern The formatted message to print.
292     * @param args The objects to insert into format string
293     * @see #logWithStackTrace(Level, Throwable)
294     */
295    public static void logWithStackTrace(Level level, Throwable t, String pattern, Object... args) {
296        logPrivate(level, () -> getErrorLogWithStack(MessageFormat.format(pattern, args), t));
297    }
298
299    private static void logPrivate(Level level, String pattern, Object... args) {
300        logPrivate(level, () -> MessageFormat.format(pattern, args));
301    }
302
303    private static void logPrivate(Level level, String message) {
304        logPrivate(level, () -> message);
305    }
306
307    private static void logPrivate(Level level, Supplier<String> supplier) {
308        // all log methods immediately call one of the logPrivate methods.
309        if (LOGGER.isLoggable(level)) {
310            StackTraceElement callingMethod = BugReport.getCallingMethod(1, Logging.class.getName(), name -> !"logPrivate".equals(name));
311            LOGGER.logp(level, callingMethod.getClassName(), callingMethod.getMethodName(), supplier);
312        }
313    }
314
315    /**
316     * Tests if a given log level is enabled. This can be used to avoid constructing debug data if required.
317     *
318     * For formatting text, you should use the {@link #debug(String, Object...)} message
319     * @param level A level constant. You can e.g. use {@link Logging#LEVEL_ERROR}
320     * @return <code>true</code> if log level is enabled.
321     */
322    public static boolean isLoggingEnabled(Level level) {
323        return LOGGER.isLoggable(level);
324    }
325
326    /**
327     * Determines if debug log level is enabled.
328     * Useful to avoid costly construction of debug messages when not enabled.
329     * @return {@code true} if log level is at least debug, {@code false} otherwise
330     * @since 12620
331     */
332    public static boolean isDebugEnabled() {
333        return isLoggingEnabled(Logging.LEVEL_DEBUG);
334    }
335
336    /**
337     * Determines if trace log level is enabled.
338     * Useful to avoid costly construction of trace messages when not enabled.
339     * @return {@code true} if log level is at least trace, {@code false} otherwise
340     * @since 12620
341     */
342    public static boolean isTraceEnabled() {
343        return isLoggingEnabled(Logging.LEVEL_TRACE);
344    }
345
346    private static String getErrorLog(String message, Throwable t) {
347        StringBuilder sb = new StringBuilder();
348        if (message != null) {
349            sb.append(message).append(": ");
350        }
351        sb.append(getErrorMessage(t));
352        return sb.toString();
353    }
354
355    private static String getErrorLogWithStack(String message, Throwable t) {
356        StringWriter sb = new StringWriter();
357        sb.append(getErrorLog(message, t));
358        if (t != null) {
359            sb.append('\n');
360            t.printStackTrace(new PrintWriter(sb));
361        }
362        return sb.toString();
363    }
364
365    /**
366     * Returns a human-readable message of error, also usable for developers.
367     * @param t The error
368     * @return The human-readable error message
369     */
370    public static String getErrorMessage(Throwable t) {
371        if (t == null) {
372            return "(no error)";
373        }
374        StringBuilder sb = new StringBuilder(t.getClass().getName());
375        String msg = t.getMessage();
376        if (msg != null) {
377            sb.append(": ").append(msg.trim());
378        }
379        Throwable cause = t.getCause();
380        if (cause != null && !cause.equals(t)) {
381            // this may cause infinite loops in the unlikely case that there is a loop in the causes.
382            sb.append(". ").append(tr("Cause: ")).append(getErrorMessage(cause));
383        }
384        return sb.toString();
385    }
386
387    /**
388     * Clear the list of last warnings
389     */
390    public static void clearLastErrorAndWarnings() {
391        WARNINGS.clear();
392    }
393
394    /**
395     * Get the last error and warning messages in the order in which they were received.
396     * @return The last errors and warnings.
397     */
398    public static List<String> getLastErrorAndWarnings() {
399        return WARNINGS.getMessages();
400    }
401
402    /**
403     * Provides direct access to the logger used. Use of methods like {@link #warn(String)} is prefered.
404     * @return The logger
405     */
406    public static Logger getLogger() {
407        return LOGGER;
408    }
409
410    private static class RememberWarningHandler extends Handler {
411        private final String[] log = new String[10];
412        private int messagesLogged;
413
414        synchronized void clear() {
415            messagesLogged = 0;
416            Arrays.fill(log, null);
417        }
418
419        @Override
420        public synchronized void publish(LogRecord record) {
421            // We don't use setLevel + isLoggable to work in WebStart Sandbox mode
422            if (record.getLevel().intValue() < LEVEL_WARN.intValue()) {
423                return;
424            }
425
426            String msg = getPrefix(record) + record.getMessage();
427
428            // Only remember first line of message
429            int idx = msg.indexOf('\n');
430            if (idx > 0) {
431                msg = msg.substring(0, idx);
432            }
433            log[messagesLogged % log.length] = msg;
434            messagesLogged++;
435        }
436
437        private static String getPrefix(LogRecord record) {
438            if (record.getLevel().equals(LEVEL_WARN)) {
439                return "W: ";
440            } else {
441                // worse than warn
442                return "E: ";
443            }
444        }
445
446        synchronized List<String> getMessages() {
447            List<String> logged = Arrays.asList(log);
448            ArrayList<String> res = new ArrayList<>();
449            int logOffset = messagesLogged % log.length;
450            if (messagesLogged > logOffset) {
451                res.addAll(logged.subList(logOffset, log.length));
452            }
453            res.addAll(logged.subList(0, logOffset));
454            return res;
455        }
456
457        @Override
458        public synchronized void flush() {
459            // nothing to do
460        }
461
462        @Override
463        public void close() {
464            // nothing to do
465        }
466    }
467}