diff --git a/src/org/openstreetmap/josm/Main.java b/src/org/openstreetmap/josm/Main.java
index a2ac9c0..c9a3b56 100644
--- a/src/org/openstreetmap/josm/Main.java
+++ b/src/org/openstreetmap/josm/Main.java
@@ -34,10 +34,6 @@ import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
-import java.util.logging.Handler;
-import java.util.logging.Level;
-import java.util.logging.LogRecord;
-import java.util.logging.Logger;
 
 import javax.swing.Action;
 import javax.swing.InputMap;
@@ -73,12 +69,13 @@ import org.openstreetmap.josm.data.projection.Projection;
 import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
 import org.openstreetmap.josm.data.validation.OsmValidator;
 import org.openstreetmap.josm.gui.GettingStarted;
-import org.openstreetmap.josm.gui.MainApplication.Option;
 import org.openstreetmap.josm.gui.MainFrame;
 import org.openstreetmap.josm.gui.MainMenu;
 import org.openstreetmap.josm.gui.MainPanel;
 import org.openstreetmap.josm.gui.MapFrame;
 import org.openstreetmap.josm.gui.MapFrameListener;
+import org.openstreetmap.josm.gui.ProgramArguments;
+import org.openstreetmap.josm.gui.ProgramArguments.Option;
 import org.openstreetmap.josm.gui.io.SaveLayersDialog;
 import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
 import org.openstreetmap.josm.gui.layer.Layer;
@@ -103,6 +100,7 @@ import org.openstreetmap.josm.plugins.PluginHandler;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 import org.openstreetmap.josm.tools.I18n;
 import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.OpenBrowser;
 import org.openstreetmap.josm.tools.OsmUrlToBounds;
 import org.openstreetmap.josm.tools.PlatformHook;
@@ -218,15 +216,14 @@ public abstract class Main {
 
     protected static final Map<String, Throwable> NETWORK_ERRORS = new HashMap<>();
 
-    // First lines of last 5 error and warning messages, used for bug reports
-    private static final List<String> ERRORS_AND_WARNINGS = Collections.<String>synchronizedList(new ArrayList<String>());
-
     private static final Set<OnlineResource> OFFLINE_RESOURCES = EnumSet.noneOf(OnlineResource.class);
 
     /**
      * Logging level (5 = trace, 4 = debug, 3 = info, 2 = warn, 1 = error, 0 = none).
      * @since 6248
+     * @deprecated Use {@link Logging} class.
      */
+    @Deprecated
     public static int logLevel = 3;
 
     /**
@@ -235,27 +232,13 @@ public abstract class Main {
      */
     protected static final MainPanel mainPanel = new MainPanel(getLayerManager());
 
-    private static void rememberWarnErrorMsg(String msg) {
-        // Only remember first line of message
-        int idx = msg.indexOf('\n');
-        if (idx > 0) {
-            ERRORS_AND_WARNINGS.add(msg.substring(0, idx));
-        } else {
-            ERRORS_AND_WARNINGS.add(msg);
-        }
-        // Only keep 10 lines to avoid memory leak
-        while (ERRORS_AND_WARNINGS.size() > 10) {
-            ERRORS_AND_WARNINGS.remove(0);
-        }
-    }
-
     /**
      * Replies the first lines of last 5 error and warning messages, used for bug reports
      * @return the first lines of last 5 error and warning messages
      * @since 7420
      */
     public static final Collection<String> getLastErrorAndWarnings() {
-        return Collections.unmodifiableList(ERRORS_AND_WARNINGS);
+        return Logging.getLastErrorAndWarnings();
     }
 
     /**
@@ -263,7 +246,7 @@ public abstract class Main {
      * @since 8959
      */
     public static void clearLastErrorAndWarnings() {
-        ERRORS_AND_WARNINGS.clear();
+        Logging.clearLastErrorAndWarnings();
     }
 
     /**
@@ -272,12 +255,7 @@ public abstract class Main {
      * @since 6248
      */
     public static void error(String msg) {
-        if (logLevel < 1)
-            return;
-        if (msg != null && !msg.isEmpty()) {
-            System.err.println(tr("ERROR: {0}", msg));
-            rememberWarnErrorMsg("E: "+msg);
-        }
+        Logging.error(msg);
     }
 
     /**
@@ -285,12 +263,7 @@ public abstract class Main {
      * @param msg The message to print.
      */
     public static void warn(String msg) {
-        if (logLevel < 2)
-            return;
-        if (msg != null && !msg.isEmpty()) {
-            System.err.println(tr("WARNING: {0}", msg));
-            rememberWarnErrorMsg("W: "+msg);
-        }
+        Logging.warn(msg);
     }
 
     /**
@@ -298,11 +271,7 @@ public abstract class Main {
      * @param msg The message to print.
      */
     public static void info(String msg) {
-        if (logLevel < 3)
-            return;
-        if (msg != null && !msg.isEmpty()) {
-            System.out.println(tr("INFO: {0}", msg));
-        }
+        Logging.info(msg);
     }
 
     /**
@@ -310,11 +279,7 @@ public abstract class Main {
      * @param msg The message to print.
      */
     public static void debug(String msg) {
-        if (logLevel < 4)
-            return;
-        if (msg != null && !msg.isEmpty()) {
-            System.out.println(tr("DEBUG: {0}", msg));
-        }
+        Logging.debug(msg);
     }
 
     /**
@@ -322,12 +287,7 @@ public abstract class Main {
      * @param msg The message to print.
      */
     public static void trace(String msg) {
-        if (logLevel < 5)
-            return;
-        if (msg != null && !msg.isEmpty()) {
-            System.out.print("TRACE: ");
-            System.out.println(msg);
-        }
+        Logging.trace(msg);
     }
 
     /**
@@ -337,7 +297,7 @@ public abstract class Main {
      * @since 6852
      */
     public static boolean isDebugEnabled() {
-        return logLevel >= 4;
+        return Logging.isLoggingEnabled(Logging.LEVEL_DEBUG);
     }
 
     /**
@@ -347,7 +307,7 @@ public abstract class Main {
      * @since 6852
      */
     public static boolean isTraceEnabled() {
-        return logLevel >= 5;
+        return Logging.isLoggingEnabled(Logging.LEVEL_TRACE);
     }
 
     /**
@@ -358,7 +318,7 @@ public abstract class Main {
      * @since 6248
      */
     public static void error(String msg, Object... objects) {
-        error(MessageFormat.format(msg, objects));
+        Logging.error(msg, objects);
     }
 
     /**
@@ -368,7 +328,7 @@ public abstract class Main {
      * @param objects The objects to insert into format string.
      */
     public static void warn(String msg, Object... objects) {
-        warn(MessageFormat.format(msg, objects));
+        Logging.warn(msg, objects);
     }
 
     /**
@@ -378,7 +338,7 @@ public abstract class Main {
      * @param objects The objects to insert into format string.
      */
     public static void info(String msg, Object... objects) {
-        info(MessageFormat.format(msg, objects));
+        Logging.info(msg, objects);
     }
 
     /**
@@ -388,7 +348,7 @@ public abstract class Main {
      * @param objects The objects to insert into format string.
      */
     public static void debug(String msg, Object... objects) {
-        debug(MessageFormat.format(msg, objects));
+        Logging.debug(msg, objects);
     }
 
     /**
@@ -398,7 +358,7 @@ public abstract class Main {
      * @param objects The objects to insert into format string.
      */
     public static void trace(String msg, Object... objects) {
-        trace(MessageFormat.format(msg, objects));
+        Logging.trace(msg, objects);
     }
 
     /**
@@ -407,7 +367,7 @@ public abstract class Main {
      * @since 6248
      */
     public static void error(Throwable t) {
-        error(t, true);
+        Logging.logWithStackTrace(Logging.LEVEL_ERROR, t);
     }
 
     /**
@@ -416,7 +376,7 @@ public abstract class Main {
      * @since 6248
      */
     public static void warn(Throwable t) {
-        warn(t, true);
+        Logging.logWithStackTrace(Logging.LEVEL_WARN, t);
     }
 
     /**
@@ -425,7 +385,7 @@ public abstract class Main {
      * @since 10420
      */
     public static void debug(Throwable t) {
-        debug(getErrorMessage(t));
+        Logging.log(Logging.LEVEL_DEBUG, t);
     }
 
     /**
@@ -434,7 +394,7 @@ public abstract class Main {
      * @since 10420
      */
     public static void trace(Throwable t) {
-        trace(getErrorMessage(t));
+        Logging.log(Logging.LEVEL_TRACE, t);
     }
 
     /**
@@ -444,9 +404,10 @@ public abstract class Main {
      * @since 6642
      */
     public static void error(Throwable t, boolean stackTrace) {
-        error(getErrorMessage(t));
         if (stackTrace) {
-            t.printStackTrace();
+            Logging.log(Logging.LEVEL_ERROR, t);
+        } else {
+            Logging.logWithStackTrace(Logging.LEVEL_ERROR, t);
         }
     }
 
@@ -457,7 +418,7 @@ public abstract class Main {
      * @since 10420
      */
     public static void error(Throwable t, String message) {
-        warn(message + ' ' + getErrorMessage(t));
+        Logging.log(Logging.LEVEL_ERROR, message, t);
     }
 
     /**
@@ -467,9 +428,10 @@ public abstract class Main {
      * @since 6642
      */
     public static void warn(Throwable t, boolean stackTrace) {
-        warn(getErrorMessage(t));
         if (stackTrace) {
-            t.printStackTrace();
+            Logging.log(Logging.LEVEL_WARN, t);
+        } else {
+            Logging.logWithStackTrace(Logging.LEVEL_WARN, t);
         }
     }
 
@@ -480,7 +442,7 @@ public abstract class Main {
      * @since 10420
      */
     public static void warn(Throwable t, String message) {
-        warn(message + ' ' + getErrorMessage(t));
+        Logging.log(Logging.LEVEL_WARN, message, t);
     }
 
     /**
@@ -490,19 +452,7 @@ public abstract class Main {
      * @since 6642
      */
     public static String getErrorMessage(Throwable t) {
-        if (t == null) {
-            return null;
-        }
-        StringBuilder sb = new StringBuilder(t.getClass().getName());
-        String msg = t.getMessage();
-        if (msg != null) {
-            sb.append(": ").append(msg.trim());
-        }
-        Throwable cause = t.getCause();
-        if (cause != null && !cause.equals(t)) {
-            sb.append(". ").append(tr("Cause: ")).append(getErrorMessage(cause));
-        }
-        return sb.toString();
+        return Logging.getErrorMessage(t);
     }
 
     /**
@@ -585,20 +535,9 @@ public abstract class Main {
         isOpenjdk = System.getProperty("java.vm.name").toUpperCase(Locale.ENGLISH).indexOf("OPENJDK") != -1;
         fileWatcher.start();
 
-        new InitializationTask(tr("Executing platform startup hook")) {
-            @Override
-            public void initialize() {
-                platform.startupHook();
-            }
-        }.call();
+        new InitializationTask(tr("Executing platform startup hook"), platform::startupHook).call();
 
-        new InitializationTask(tr("Building main menu")) {
-
-            @Override
-            public void initialize() {
-                initializeMainWindow();
-            }
-        }.call();
+        new InitializationTask(tr("Building main menu"), this::initializeMainWindow).call();
 
         undoRedo.addCommandQueueListener(redoUndoListener);
 
@@ -611,10 +550,7 @@ public abstract class Main {
         // contains several initialization tasks to be executed (in parallel) by a ExecutorService
         List<Callable<Void>> tasks = new ArrayList<>();
 
-        tasks.add(new InitializationTask(tr("Initializing OSM API")) {
-
-            @Override
-            public void initialize() {
+        tasks.add(new InitializationTask(tr("Initializing OSM API"), () -> {
                 // We try to establish an API connection early, so that any API
                 // capabilities are already known to the editor instance. However
                 // if it goes wrong that's not critical at this stage.
@@ -623,43 +559,18 @@ public abstract class Main {
                 } catch (OsmTransferCanceledException | OsmApiInitializationException e) {
                     Main.warn(getErrorMessage(Utils.getRootCause(e)));
                 }
-            }
-        });
-
-        tasks.add(new InitializationTask(tr("Initializing validator")) {
+            }));
 
-            @Override
-            public void initialize() {
-                OsmValidator.initialize();
-            }
-        });
+        tasks.add(new InitializationTask(tr("Initializing validator"), OsmValidator::initialize));
 
-        tasks.add(new InitializationTask(tr("Initializing presets")) {
+        tasks.add(new InitializationTask(tr("Initializing presets"), TaggingPresets::initialize));
 
-            @Override
-            public void initialize() {
-                TaggingPresets.initialize();
-            }
-        });
+        tasks.add(new InitializationTask(tr("Initializing map styles"), MapPaintPreference::initialize));
 
-        tasks.add(new InitializationTask(tr("Initializing map styles")) {
-
-            @Override
-            public void initialize() {
-                MapPaintPreference.initialize();
-            }
-        });
-
-        tasks.add(new InitializationTask(tr("Loading imagery preferences")) {
-
-            @Override
-            public void initialize() {
-                ImageryPreference.initialize();
-            }
-        });
+        tasks.add(new InitializationTask(tr("Loading imagery preferences"), ImageryPreference::initialize));
 
         try {
-            final ExecutorService service = Executors.newFixedThreadPool(
+            ExecutorService service = Executors.newFixedThreadPool(
                     Runtime.getRuntime().availableProcessors(), Utils.newThreadFactory("main-init-%d", Thread.NORM_PRIORITY));
             for (Future<Void> i : service.invokeAll(tasks)) {
                 i.get();
@@ -672,51 +583,13 @@ public abstract class Main {
         // hooks for the jmapviewer component
         FeatureAdapter.registerBrowserAdapter(OpenBrowser::displayUrl);
         FeatureAdapter.registerTranslationAdapter(I18n.getTranslationAdapter());
-        FeatureAdapter.registerLoggingAdapter(name -> {
-                Logger logger = Logger.getAnonymousLogger();
-                logger.setUseParentHandlers(false);
-                logger.setLevel(Level.ALL);
-                if (logger.getHandlers().length == 0) {
-                    logger.addHandler(new Handler() {
-                        @Override
-                        public void publish(LogRecord record) {
-                            String msg = MessageFormat.format(record.getMessage(), record.getParameters());
-                            if (record.getLevel().intValue() >= Level.SEVERE.intValue()) {
-                                Main.error(msg);
-                            } else if (record.getLevel().intValue() >= Level.WARNING.intValue()) {
-                                Main.warn(msg);
-                            } else if (record.getLevel().intValue() >= Level.INFO.intValue()) {
-                                Main.info(msg);
-                            } else if (record.getLevel().intValue() >= Level.FINE.intValue()) {
-                                Main.debug(msg);
-                            } else {
-                                Main.trace(msg);
-                            }
-                        }
-
-                        @Override
-                        public void flush() {
-                            // Do nothing
-                        }
-
-                        @Override
-                        public void close() {
-                            // Do nothing
-                        }
-                    });
-                }
-                return logger;
-            });
+        FeatureAdapter.registerLoggingAdapter(name -> Logging.getLogger());
 
-        new InitializationTask(tr("Updating user interface")) {
-
-            @Override
-            public void initialize() {
-                toolbar.refreshToolbarControl();
-                toolbar.control.updateUI();
-                contentPanePrivate.updateUI();
-            }
-        }.call();
+        new InitializationTask(tr("Updating user interface"), () -> {
+            toolbar.refreshToolbarControl();
+            toolbar.control.updateUI();
+            contentPanePrivate.updateUI();
+        }).call();
     }
 
     /**
@@ -727,23 +600,23 @@ public abstract class Main {
         // can be implementd by subclasses
     }
 
-    private abstract static class InitializationTask implements Callable<Void> {
+    private static class InitializationTask implements Callable<Void> {
 
         private final String name;
+        private Runnable task;
 
-        protected InitializationTask(String name) {
+        protected InitializationTask(String name, Runnable task) {
             this.name = name;
+            this.task = task;
         }
 
-        public abstract void initialize();
-
         @Override
         public Void call() {
             Object status = null;
             if (initListener != null) {
                 status = initListener.updateStatus(name);
             }
-            initialize();
+            task.run();
             if (initListener != null) {
                 initListener.finish(status);
             }
@@ -965,7 +838,7 @@ public abstract class Main {
      * Should be called before the main constructor to setup some parameter stuff
      * @param args The parsed argument list.
      */
-    public static void preConstructorInit(Map<Option, Collection<String>> args) {
+    public static void preConstructorInit(ProgramArguments args) {
         ProjectionPreference.setProjection();
 
         String defaultlaf = platform.getDefaultStyle();
@@ -1032,25 +905,19 @@ public abstract class Main {
         }
     }
 
-    protected static void postConstructorProcessCmdLine(Map<Option, Collection<String>> args) {
-        if (args.containsKey(Option.DOWNLOAD)) {
-            List<File> fileList = new ArrayList<>();
-            for (String s : args.get(Option.DOWNLOAD)) {
-                DownloadParamType.paramType(s).download(s, fileList);
-            }
-            if (!fileList.isEmpty()) {
-                OpenFileAction.openFiles(fileList, true);
-            }
+    protected static void postConstructorProcessCmdLine(ProgramArguments args) {
+        List<File> fileList = new ArrayList<>();
+        for (String s : args.get(Option.DOWNLOAD)) {
+            DownloadParamType.paramType(s).download(s, fileList);
         }
-        if (args.containsKey(Option.DOWNLOADGPS)) {
-            for (String s : args.get(Option.DOWNLOADGPS)) {
-                DownloadParamType.paramType(s).downloadGps(s);
-            }
+        if (!fileList.isEmpty()) {
+            OpenFileAction.openFiles(fileList, true);
         }
-        if (args.containsKey(Option.SELECTION)) {
-            for (String s : args.get(Option.SELECTION)) {
-                SearchAction.search(s, SearchAction.SearchMode.add);
-            }
+        for (String s : args.get(Option.DOWNLOADGPS)) {
+            DownloadParamType.paramType(s).downloadGps(s);
+        }
+        for (String s : args.get(Option.SELECTION)) {
+            SearchAction.search(s, SearchAction.SearchMode.add);
         }
     }
 
@@ -1503,10 +1370,8 @@ public abstract class Main {
         public static void setup() {
             if (!windowSwitchListeners.isEmpty()) {
                 for (Window w : Window.getWindows()) {
-                    if (w.isShowing()) {
-                        if (!Arrays.asList(w.getWindowListeners()).contains(getInstance())) {
-                            w.addWindowListener(getInstance());
-                        }
+                    if (w.isShowing() && !Arrays.asList(w.getWindowListeners()).contains(getInstance())) {
+                        w.addWindowListener(getInstance());
                     }
                 }
             }
diff --git a/src/org/openstreetmap/josm/gui/MainApplication.java b/src/org/openstreetmap/josm/gui/MainApplication.java
index b65e16c..c730abf 100644
--- a/src/org/openstreetmap/josm/gui/MainApplication.java
+++ b/src/org/openstreetmap/josm/gui/MainApplication.java
@@ -24,15 +24,14 @@ import java.security.PermissionCollection;
 import java.security.Permissions;
 import java.security.Policy;
 import java.security.cert.CertificateException;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.EnumMap;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.logging.Level;
 
 import javax.swing.JOptionPane;
 import javax.swing.RepaintManager;
@@ -45,6 +44,7 @@ import org.openstreetmap.josm.actions.RestartAction;
 import org.openstreetmap.josm.data.AutosaveTask;
 import org.openstreetmap.josm.data.CustomConfigurator;
 import org.openstreetmap.josm.data.Version;
+import org.openstreetmap.josm.gui.ProgramArguments.Option;
 import org.openstreetmap.josm.gui.SplashScreen.SplashProgressMonitor;
 import org.openstreetmap.josm.gui.download.DownloadDialog;
 import org.openstreetmap.josm.gui.preferences.server.OAuthAccessTokenHolder;
@@ -62,15 +62,14 @@ import org.openstreetmap.josm.plugins.PluginInformation;
 import org.openstreetmap.josm.tools.FontsManager;
 import org.openstreetmap.josm.tools.HttpClient;
 import org.openstreetmap.josm.tools.I18n;
+import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.OsmUrlToBounds;
 import org.openstreetmap.josm.tools.PlatformHookWindows;
 import org.openstreetmap.josm.tools.Utils;
 import org.openstreetmap.josm.tools.WindowGeometry;
+import org.openstreetmap.josm.tools.bugreport.BugReport;
 import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler;
 
-import gnu.getopt.Getopt;
-import gnu.getopt.LongOpt;
-
 /**
  * Main window class application.
  *
@@ -170,128 +169,6 @@ public class MainApplication extends Main {
     }
 
     /**
-     * JOSM command line options.
-     * @see <a href="https://josm.openstreetmap.de/wiki/Help/CommandLineOptions">Help/CommandLineOptions</a>
-     * @since 5279
-     */
-    public enum Option {
-        /** --help|-h                                  Show this help */
-        HELP(false),
-        /** --version                                  Displays the JOSM version and exits */
-        VERSION(false),
-        /** --debug                                    Print debugging messages to console */
-        DEBUG(false),
-        /** --trace                                    Print detailed debugging messages to console */
-        TRACE(false),
-        /** --language=&lt;language&gt;                Set the language */
-        LANGUAGE(true),
-        /** --reset-preferences                        Reset the preferences to default */
-        RESET_PREFERENCES(false),
-        /** --load-preferences=&lt;url-to-xml&gt;      Changes preferences according to the XML file */
-        LOAD_PREFERENCES(true),
-        /** --set=&lt;key&gt;=&lt;value&gt;            Set preference key to value */
-        SET(true),
-        /** --geometry=widthxheight(+|-)x(+|-)y        Standard unix geometry argument */
-        GEOMETRY(true),
-        /** --no-maximize                              Do not launch in maximized mode */
-        NO_MAXIMIZE(false),
-        /** --maximize                                 Launch in maximized mode */
-        MAXIMIZE(false),
-        /** --download=minlat,minlon,maxlat,maxlon     Download the bounding box <br>
-         *  --download=&lt;URL&gt;                     Download the location at the URL (with lat=x&amp;lon=y&amp;zoom=z) <br>
-         *  --download=&lt;filename&gt;                Open a file (any file type that can be opened with File/Open) */
-        DOWNLOAD(true),
-        /** --downloadgps=minlat,minlon,maxlat,maxlon  Download the bounding box as raw GPS <br>
-         *  --downloadgps=&lt;URL&gt;                  Download the location at the URL (with lat=x&amp;lon=y&amp;zoom=z) as raw GPS */
-        DOWNLOADGPS(true),
-        /** --selection=&lt;searchstring&gt;           Select with the given search */
-        SELECTION(true),
-        /** --offline=&lt;osm_api|josm_website|all&gt; Disable access to the given resource(s), delimited by comma */
-        OFFLINE(true),
-        /** --skip-plugins */
-        SKIP_PLUGINS(false);
-
-        private final String name;
-        private final boolean requiresArg;
-
-        Option(boolean requiresArgument) {
-            this.name = name().toLowerCase(Locale.ENGLISH).replace('_', '-');
-            this.requiresArg = requiresArgument;
-        }
-
-        /**
-         * Replies the option name
-         * @return The option name, in lowercase
-         */
-        public String getName() {
-            return name;
-        }
-
-        /**
-         * Determines if this option requires an argument.
-         * @return {@code true} if this option requires an argument, {@code false} otherwise
-         */
-        public boolean requiresArgument() {
-            return requiresArg;
-        }
-    }
-
-    /**
-     * Builds the command-line argument map.
-     * @param args command-line arguments array
-     * @return command-line argument map
-     */
-    public static Map<Option, Collection<String>> buildCommandLineArgumentMap(String ... args) {
-
-        List<LongOpt> los = new ArrayList<>();
-        for (Option o : Option.values()) {
-            los.add(new LongOpt(o.getName(), o.requiresArgument() ? LongOpt.REQUIRED_ARGUMENT : LongOpt.NO_ARGUMENT, null, 0));
-        }
-
-        Getopt g = new Getopt("JOSM", args, "hv", los.toArray(new LongOpt[los.size()]));
-
-        Map<Option, Collection<String>> argMap = new EnumMap<>(Option.class);
-
-        int c;
-        while ((c = g.getopt()) != -1) {
-            Option opt;
-            switch (c) {
-                case 'h':
-                    opt = Option.HELP;
-                    break;
-                case 'v':
-                    opt = Option.VERSION;
-                    break;
-                case 0:
-                    opt = Option.values()[g.getLongind()];
-                    break;
-                default:
-                    opt = null;
-            }
-            if (opt != null) {
-                Collection<String> values = argMap.get(opt);
-                if (values == null) {
-                    values = new ArrayList<>();
-                    argMap.put(opt, values);
-                }
-                values.add(g.getOptarg());
-            } else
-                throw new IllegalArgumentException("Invalid option: "+c);
-        }
-        // positional arguments are a shortcut for the --download ... option
-        for (int i = g.getOptind(); i < args.length; ++i) {
-            Collection<String> values = argMap.get(Option.DOWNLOAD);
-            if (values == null) {
-                values = new ArrayList<>();
-                argMap.put(Option.DOWNLOAD, values);
-            }
-            values.add(args[i]);
-        }
-
-        return argMap;
-    }
-
-    /**
      * Main application Startup
      * @param argArray Command-line arguments
      */
@@ -299,19 +176,20 @@ public class MainApplication extends Main {
         I18n.init();
 
         // construct argument table
-        Map<Option, Collection<String>> args = null;
+        ProgramArguments args = null;
         try {
-            args = buildCommandLineArgumentMap(argArray);
+            args = new ProgramArguments(argArray);
         } catch (IllegalArgumentException e) {
             System.exit(1);
             return;
         }
 
-        final boolean languageGiven = args.containsKey(Option.LANGUAGE);
+        Level logLevel = args.getLogLevel();
+        Logging.setLogLevel(logLevel);
+        Main.info(tr("Log level is at ", logLevel));
 
-        if (languageGiven) {
-            I18n.set(args.get(Option.LANGUAGE).iterator().next());
-        }
+        Optional<String> language = args.getSingle(Option.LANGUAGE);
+        I18n.set(language.orElse(null));
 
         initApplicationPreferences();
 
@@ -339,41 +217,31 @@ public class MainApplication extends Main {
 
         Main.COMMAND_LINE_ARGS.addAll(Arrays.asList(argArray));
 
-        if (args.containsKey(Option.VERSION)) {
+        if (args.showVersion()) {
             System.out.println(Version.getInstance().getAgentString());
             System.exit(0);
-        }
-
-        if (args.containsKey(Option.DEBUG) || args.containsKey(Option.TRACE)) {
-            // Enable JOSM debug level
-            logLevel = 4;
-            Main.info(tr("Printing debugging messages to console"));
+        } else if (args.showHelp()) {
+            showHelp();
+            System.exit(0);
         }
 
         boolean skipLoadingPlugins = false;
-        if (args.containsKey(Option.SKIP_PLUGINS)) {
+        if (args.hasOption(Option.SKIP_PLUGINS)) {
             skipLoadingPlugins = true;
             Main.info(tr("Plugin loading skipped"));
         }
 
-        if (args.containsKey(Option.TRACE)) {
-            // Enable JOSM debug level
-            logLevel = 5;
+        if (Logging.isLoggingEnabled(Logging.LEVEL_TRACE)) {
             // Enable debug in OAuth signpost via system preference, but only at trace level
             Utils.updateSystemProperty("debug", "true");
             Main.info(tr("Enabled detailed debug level (trace)"));
         }
 
-        Main.pref.init(args.containsKey(Option.RESET_PREFERENCES));
+        Main.pref.init(args.hasOption(Option.RESET_PREFERENCES));
 
-        if (args.containsKey(Option.SET)) {
-            for (String i : args.get(Option.SET)) {
-                String[] kv = i.split("=", 2);
-                Main.pref.put(kv[0], "null".equals(kv[1]) ? null : kv[1]);
-            }
-        }
+        args.getPreferencesToSet().forEach(Main.pref::put);
 
-        if (!languageGiven) {
+        if (!language.isPresent()) {
             I18n.set(Main.pref.get("language", null));
         }
         Main.pref.updateSystemProperties();
@@ -381,7 +249,7 @@ public class MainApplication extends Main {
         checkIPv6();
 
         // asking for help? show help and exit
-        if (args.containsKey(Option.HELP)) {
+        if (args.hasOption(Option.HELP)) {
             showHelp();
             System.exit(0);
         }
@@ -395,19 +263,19 @@ public class MainApplication extends Main {
         I18n.setupLanguageFonts();
 
         WindowGeometry geometry = WindowGeometry.mainWindow("gui.geometry",
-                args.containsKey(Option.GEOMETRY) ? args.get(Option.GEOMETRY).iterator().next() : null,
-                !args.containsKey(Option.NO_MAXIMIZE) && Main.pref.getBoolean("gui.maximized", false));
+                args.getSingle(Option.GEOMETRY).orElse(null),
+                !args.hasOption(Option.NO_MAXIMIZE) && Main.pref.getBoolean("gui.maximized", false));
         final MainFrame mainFrame = new MainFrame(contentPanePrivate, mainPanel, geometry);
         Main.parent = mainFrame;
 
-        if (args.containsKey(Option.LOAD_PREFERENCES)) {
+        if (args.hasOption(Option.LOAD_PREFERENCES)) {
             CustomConfigurator.XMLCommandProcessor config = new CustomConfigurator.XMLCommandProcessor(Main.pref);
             for (String i : args.get(Option.LOAD_PREFERENCES)) {
                 info("Reading preferences from " + i);
                 try (InputStream is = HttpClient.create(new URL(i)).connect().getContent()) {
                     config.openAndReadXML(is);
                 } catch (IOException ex) {
-                    throw new RuntimeException(ex);
+                    throw BugReport.intercept(ex).put("file", i);
                 }
             }
         }
@@ -470,7 +338,7 @@ public class MainApplication extends Main {
         Main.MasterWindowListener.setup();
 
         boolean maximized = Main.pref.getBoolean("gui.maximized", false);
-        if ((!args.containsKey(Option.NO_MAXIMIZE) && maximized) || args.containsKey(Option.MAXIMIZE)) {
+        if ((!args.hasOption(Option.NO_MAXIMIZE) && maximized) || args.hasOption(Option.MAXIMIZE)) {
             mainFrame.setMaximized(true);
         }
         if (main.menu.fullscreenToggleAction != null) {
@@ -528,9 +396,9 @@ public class MainApplication extends Main {
         toolbar.refreshToolbarControl();
     }
 
-    private static void processOffline(Map<Option, Collection<String>> args) {
-        if (args.containsKey(Option.OFFLINE)) {
-            for (String s : args.get(Option.OFFLINE).iterator().next().split(",")) {
+    private static void processOffline(ProgramArguments args) {
+        for (String offlineNames : args.get(Option.OFFLINE)) {
+            for (String s : offlineNames.split(",")) {
                 try {
                     Main.setOffline(OnlineResource.valueOf(s.toUpperCase(Locale.ENGLISH)));
                 } catch (IllegalArgumentException e) {
@@ -540,12 +408,12 @@ public class MainApplication extends Main {
                     return;
                 }
             }
-            Set<OnlineResource> offline = Main.getOfflineResources();
-            if (!offline.isEmpty()) {
-                Main.warn(trn("JOSM is running in offline mode. This resource will not be available: {0}",
-                        "JOSM is running in offline mode. These resources will not be available: {0}",
-                        offline.size(), offline.size() == 1 ? offline.iterator().next() : Arrays.toString(offline.toArray())));
-            }
+        }
+        Set<OnlineResource> offline = Main.getOfflineResources();
+        if (!offline.isEmpty()) {
+            Main.warn(trn("JOSM is running in offline mode. This resource will not be available: {0}",
+                    "JOSM is running in offline mode. These resources will not be available: {0}",
+                    offline.size(), offline.size() == 1 ? offline.iterator().next() : Arrays.toString(offline.toArray())));
         }
     }
 
@@ -600,10 +468,10 @@ public class MainApplication extends Main {
 
     private static class GuiFinalizationWorker implements Runnable {
 
-        private final Map<Option, Collection<String>> args;
+        private final ProgramArguments args;
         private final DefaultProxySelector proxySelector;
 
-        GuiFinalizationWorker(Map<Option, Collection<String>> args, DefaultProxySelector proxySelector) {
+        GuiFinalizationWorker(ProgramArguments args, DefaultProxySelector proxySelector) {
             this.args = args;
             this.proxySelector = proxySelector;
         }
diff --git a/src/org/openstreetmap/josm/gui/ProgramArguments.java b/src/org/openstreetmap/josm/gui/ProgramArguments.java
new file mode 100644
index 0000000..cf446f2
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/ProgramArguments.java
@@ -0,0 +1,227 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.logging.Level;
+import java.util.stream.Stream;
+
+import org.openstreetmap.josm.tools.Logging;
+
+import gnu.getopt.Getopt;
+import gnu.getopt.LongOpt;
+
+/**
+ * This class holds the arguments passed on to Main.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public class ProgramArguments {
+
+    /**
+     * JOSM command line options.
+     * @see <a href="https://josm.openstreetmap.de/wiki/Help/CommandLineOptions">Help/CommandLineOptions</a>
+     * @since xxx
+     */
+    public enum Option {
+        /** --help|-h                                  Show this help */
+        HELP(false),
+        /** --version                                  Displays the JOSM version and exits */
+        VERSION(false),
+        /** --debug                                    Print debugging messages to console */
+        DEBUG(false),
+        /** --trace                                    Print detailed debugging messages to console */
+        TRACE(false),
+        /** --language=&lt;language&gt;                Set the language */
+        LANGUAGE(true),
+        /** --reset-preferences                        Reset the preferences to default */
+        RESET_PREFERENCES(false),
+        /** --load-preferences=&lt;url-to-xml&gt;      Changes preferences according to the XML file */
+        LOAD_PREFERENCES(true),
+        /** --set=&lt;key&gt;=&lt;value&gt;            Set preference key to value */
+        SET(true),
+        /** --geometry=widthxheight(+|-)x(+|-)y        Standard unix geometry argument */
+        GEOMETRY(true),
+        /** --no-maximize                              Do not launch in maximized mode */
+        NO_MAXIMIZE(false),
+        /** --maximize                                 Launch in maximized mode */
+        MAXIMIZE(false),
+        /** --download=minlat,minlon,maxlat,maxlon     Download the bounding box <br>
+         *  --download=&lt;URL&gt;                     Download the location at the URL (with lat=x&amp;lon=y&amp;zoom=z) <br>
+         *  --download=&lt;filename&gt;                Open a file (any file type that can be opened with File/Open) */
+        DOWNLOAD(true),
+        /** --downloadgps=minlat,minlon,maxlat,maxlon  Download the bounding box as raw GPS <br>
+         *  --downloadgps=&lt;URL&gt;                  Download the location at the URL (with lat=x&amp;lon=y&amp;zoom=z) as raw GPS */
+        DOWNLOADGPS(true),
+        /** --selection=&lt;searchstring&gt;           Select with the given search */
+        SELECTION(true),
+        /** --offline=&lt;osm_api|josm_website|all&gt; Disable access to the given resource(s), delimited by comma */
+        OFFLINE(true),
+        /** --skip-plugins */
+        SKIP_PLUGINS(false);
+
+        private final String name;
+        private final boolean requiresArg;
+
+        Option(boolean requiresArgument) {
+            this.name = name().toLowerCase(Locale.ENGLISH).replace('_', '-');
+            this.requiresArg = requiresArgument;
+        }
+
+        /**
+         * Replies the option name
+         * @return The option name, in lowercase
+         */
+        public String getName() {
+            return name;
+        }
+
+        /**
+         * Determines if this option requires an argument.
+         * @return {@code true} if this option requires an argument, {@code false} otherwise
+         */
+        public boolean requiresArgument() {
+            return requiresArg;
+        }
+
+        LongOpt toLongOpt() {
+            return new LongOpt(getName(), requiresArgument() ? LongOpt.REQUIRED_ARGUMENT : LongOpt.NO_ARGUMENT, null, 0);
+        }
+    }
+
+    private final Map<Option, List<String>> argMap = new EnumMap<>(Option.class);
+
+    /**
+     * Construct the program arguments object
+     * @param args The args passed to main.
+     */
+    public ProgramArguments(String[] args) {
+        Stream.of(Option.values()).forEach(o -> argMap.put(o, new ArrayList<>()));
+
+        buildCommandLineArgumentMap(args);
+    }
+
+    /**
+     * Builds the command-line argument map.
+     * @param args command-line arguments array
+     */
+    private void buildCommandLineArgumentMap(String ... args) {
+        LongOpt[] los = Stream.of(Option.values()).map(Option::toLongOpt).toArray(i -> new LongOpt[i]);
+
+        Getopt g = new Getopt("JOSM", args, "hv", los);
+
+        int c;
+        while ((c = g.getopt()) != -1) {
+            Option opt;
+            switch (c) {
+            case 'h':
+                opt = Option.HELP;
+                break;
+            case 'v':
+                opt = Option.VERSION;
+                break;
+            case 0:
+                opt = Option.values()[g.getLongind()];
+                break;
+            default:
+                opt = null;
+            }
+            if (opt != null) {
+                addOption(opt, g.getOptarg());
+            } else
+                throw new IllegalArgumentException("Invalid option: "+c);
+        }
+        // positional arguments are a shortcut for the --download ... option
+        for (int i = g.getOptind(); i < args.length; ++i) {
+            addOption(Option.DOWNLOAD, args[i]);
+        }
+    }
+
+    private void addOption(Option opt, String optarg) {
+        argMap.get(opt).add(optarg);
+    }
+
+    /**
+     * Gets a single argument (the first) that was given for the given option.
+     * @param option The option to search
+     * @return The argument as optional value.
+     */
+    public Optional<String> getSingle(Option option) {
+        return get(option).stream().findFirst();
+    }
+
+    /**
+     * Gets all values that are given for a given option
+     * @param option The option
+     * @return The values that were given. May be empty.
+     */
+    public Collection<String> get(Option option) {
+        return Collections.unmodifiableList(argMap.get(option));
+    }
+
+    /**
+     * Test if a given option was used by the user.
+     * @param option The option to test for
+     * @return <code>true</code> if the user used it.
+     */
+    public boolean hasOption(Option option) {
+        return !get(option).isEmpty();
+    }
+
+    /**
+     * Helper method to indicate if version should be displayed.
+     * @return <code>true</code> to display version
+     */
+    public boolean showVersion() {
+        return hasOption(Option.VERSION);
+    }
+
+    /**
+     * Helper method to indicate if help should be displayed.
+     * @return <code>true</code> to display version
+     */
+    public boolean showHelp() {
+        return !get(Option.HELP).isEmpty();
+    }
+
+    /**
+     * Get the log level the user wants us to use.
+     * @return The log level.
+     */
+    public Level getLogLevel() {
+        if (hasOption(Option.TRACE)) {
+            return Logging.LEVEL_TRACE;
+        } else if (hasOption(Option.DEBUG)) {
+            return Logging.LEVEL_DEBUG;
+        } else {
+            return Logging.LEVEL_INFO;
+        }
+    }
+
+    /**
+     * Gets a map of all preferences the user wants to set.
+     * @return The preferences to set. It contains null values for preferences to unset
+     */
+    public Map<String, String> getPreferencesToSet() {
+        HashMap<String, String> map = new HashMap<>();
+        get(Option.SET).stream().map(i -> i.split("=", 2)).forEach(kv -> map.put(kv[0], getValue(kv)));
+        return map;
+    }
+
+    private static String getValue(String[] kv) {
+        if (kv.length < 2) {
+            return "";
+        } else if ("null".equals(kv[1])) {
+            return null;
+        } else {
+            return kv[1];
+        }
+    }
+}
diff --git a/src/org/openstreetmap/josm/tools/Logging.java b/src/org/openstreetmap/josm/tools/Logging.java
new file mode 100644
index 0000000..db6855f
--- /dev/null
+++ b/src/org/openstreetmap/josm/tools/Logging.java
@@ -0,0 +1,369 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import org.openstreetmap.josm.tools.bugreport.BugReport;
+
+/**
+ * This class contains utility methods to log errors and warnings.
+ * <p>
+ * There are multiple log levels supported.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public final class Logging {
+    /**
+     * The josm internal log level indicating a severe error in the application that usually leads to a crash.
+     */
+    public static final Level LEVEL_ERROR = Level.SEVERE;
+    /**
+     * The josm internal log level to use when something that may lead to a crash or wrong behaviour has happened.
+     */
+    public static final Level LEVEL_WARN = Level.WARNING;
+    /**
+     * The josm internal log level to use for important events that will be useful when debugging problems
+     */
+    public static final Level LEVEL_INFO = Level.INFO;
+    /**
+     * The josm internal log level to print debug output
+     */
+    public static final Level LEVEL_DEBUG = Level.FINE;
+    /**
+     * The finest log level josm supports. This lets josm print a lot of debug output.
+     */
+    public static final Level LEVEL_TRACE = Level.FINEST;
+    private static final Logger LOGGER = Logger.getAnonymousLogger();
+    private static final RememberWarningHandler WARNINGS = new RememberWarningHandler();
+
+    static {
+        LOGGER.setLevel(Level.ALL);
+        LOGGER.setUseParentHandlers(false);
+
+        ConsoleHandler stderr = new ConsoleHandler();
+        LOGGER.addHandler(stderr);
+        stderr.setLevel(LEVEL_WARN);
+
+        ConsoleHandler stdout = new ConsoleHandler() {
+            @Override
+            protected synchronized void setOutputStream(OutputStream out) {
+                // overwrite output stream.
+                super.setOutputStream(System.out);
+            }
+
+            @Override
+            public void publish(LogRecord record) {
+                if (!stderr.isLoggable(record)) {
+                    super.publish(record);
+                }
+            }
+        };
+        LOGGER.addHandler(stdout);
+        stdout.setLevel(Level.ALL);
+
+        LOGGER.addHandler(WARNINGS);
+    }
+
+    private Logging() {
+        // hide
+    }
+
+    /**
+     * Set the global log level.
+     * @param level The log level to use
+     */
+    public static void setLogLevel(Level level) {
+        LOGGER.setLevel(level);
+    }
+
+    /**
+     * Prints an error message if logging is on.
+     * @param message The message to print.
+     */
+    public static void error(String message) {
+        logPrivate(LEVEL_ERROR, message);
+    }
+
+    /**
+     * Prints a formatted error message if logging is on. Calls {@link MessageFormat#format}
+     * function to format text.
+     * @param pattern The formatted message to print.
+     * @param args The objects to insert into format string.
+     */
+    public static void error(String pattern, Object... args) {
+        logPrivate(LEVEL_ERROR, pattern, args);
+    }
+
+    /**
+     * Prints a warning message if logging is on.
+     * @param message The message to print.
+     */
+    public static void warn(String message) {
+        logPrivate(LEVEL_WARN, message);
+    }
+
+    /**
+     * Prints a formatted warning message if logging is on. Calls {@link MessageFormat#format}
+     * function to format text.
+     * @param pattern The formatted message to print.
+     * @param args The objects to insert into format string.
+     */
+    public static void warn(String pattern, Object... args) {
+        logPrivate(LEVEL_WARN, pattern, args);
+    }
+
+    /**
+     * Prints a info message if logging is on.
+     * @param message The message to print.
+     */
+    public static void info(String message) {
+        logPrivate(LEVEL_INFO, message);
+    }
+
+    /**
+     * Prints a formatted info message if logging is on. Calls {@link MessageFormat#format}
+     * function to format text.
+     * @param pattern The formatted message to print.
+     * @param args The objects to insert into format string.
+     */
+    public static void info(String pattern, Object... args) {
+        logPrivate(LEVEL_INFO, pattern, args);
+    }
+
+    /**
+     * Prints a debug message if logging is on.
+     * @param message The message to print.
+     */
+    public static void debug(String message) {
+        logPrivate(LEVEL_DEBUG, message);
+    }
+
+    /**
+     * Prints a formatted debug message if logging is on. Calls {@link MessageFormat#format}
+     * function to format text.
+     * @param pattern The formatted message to print.
+     * @param args The objects to insert into format string.
+     */
+    public static void debug(String pattern, Object... args) {
+        logPrivate(LEVEL_DEBUG, pattern, args);
+    }
+
+    /**
+     * Prints a trace message if logging is on.
+     * @param message The message to print.
+     */
+    public static void trace(String message) {
+        logPrivate(LEVEL_TRACE, message);
+    }
+
+    /**
+     * Prints a formatted trace message if logging is on. Calls {@link MessageFormat#format}
+     * function to format text.
+     * @param pattern The formatted message to print.
+     * @param args The objects to insert into format string.
+     */
+    public static void trace(String pattern, Object... args) {
+        logPrivate(LEVEL_TRACE, pattern, args);
+    }
+
+    /**
+     * Logs a throwable that happened.
+     * @param level The level.
+     * @param t The throwable that should be logged.
+     */
+    public static void log(Level level, Throwable t) {
+        logPrivate(level, () -> getErrorLog(null, t));
+    }
+
+    /**
+     * Logs a throwable that happened.
+     * @param level The level.
+     * @param message An additional error message
+     * @param t The throwable that caused the message
+     */
+    public static void log(Level level, String message, Throwable t) {
+        logPrivate(level, () -> getErrorLog(message, t));
+    }
+
+    /**
+     * Logs a throwable that happened. Adds the stack trace to the log.
+     * @param level The level.
+     * @param t The throwable that should be logged.
+     */
+    public static void logWithStackTrace(Level level, Throwable t) {
+        logPrivate(level, () -> getErrorLogWithStack(null, t));
+    }
+
+    /**
+     * Logs a throwable that happened. Adds the stack trace to the log.
+     * @param level The level.
+     * @param message An additional error message
+     * @param t The throwable that should be logged.
+     */
+    public static void logWithStackTrace(Level level, String message, Throwable t) {
+        logPrivate(level, () -> getErrorLogWithStack(message, t));
+    }
+
+    private static void logPrivate(Level level, String pattern, Object... args) {
+        logPrivate(level, () -> MessageFormat.format(pattern, args));
+    }
+
+    private static void logPrivate(Level level, String message) {
+        logPrivate(level, () -> message);
+    }
+
+    private static void logPrivate(Level level, Supplier<String> supplier) {
+        // all log methods immeadiately call one of the logPrivate methods.
+        if (LOGGER.isLoggable(level)) {
+            StackTraceElement callingMethod = BugReport.getCallingMethod(1, Logging.class.getName(), name -> !"logPrivate".equals(name));
+            LOGGER.logp(level, callingMethod.getClassName(), callingMethod.getMethodName(), supplier);
+        }
+    }
+
+    /**
+     * Tests if a given log level is enabled. This can be used to avoid constructing debug data if required.
+     *
+     * For formatting text, you should use the {@link #debug(String, Object...)} message
+     * @param level A lvele constant. You can e.g. use {@link Logging#LEVEL_ERROR}
+     * @return <code>true</code> if debug is enabled.
+     */
+    public static boolean isLoggingEnabled(Level level) {
+        return LOGGER.isLoggable(level);
+    }
+
+    private static String getErrorLog(String message, Throwable t) {
+        StringBuilder sb = new StringBuilder();
+        if (message != null) {
+            sb.append(message).append(": ");
+        }
+        sb.append(getErrorMessage(t));
+        return sb.toString();
+    }
+
+    private static String getErrorLogWithStack(String message, Throwable t) {
+        StringWriter sb = new StringWriter();
+        sb.append(getErrorLog(message, t));
+        sb.append('\n');
+        t.printStackTrace(new PrintWriter(sb));
+        return sb.toString();
+    }
+
+    /**
+     * Returns a human-readable message of error, also usable for developers.
+     * @param t The error
+     * @return The human-readable error message
+     */
+    public static String getErrorMessage(Throwable t) {
+        if (t == null) {
+            return "(no error)";
+        }
+        StringBuilder sb = new StringBuilder(t.getClass().getName());
+        String msg = t.getMessage();
+        if (msg != null) {
+            sb.append(": ").append(msg.trim());
+        }
+        Throwable cause = t.getCause();
+        if (cause != null && !cause.equals(t)) {
+            sb.append(". ").append(tr("Cause: ")).append(getErrorMessage(cause));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Clear the list of last warnings
+     */
+    public static void clearLastErrorAndWarnings() {
+        WARNINGS.clear();
+    }
+
+    /**
+     * Get the last error and warning messages in the order in which they were received.
+     * @return The last errors and warnings.
+     */
+    public static List<String> getLastErrorAndWarnings() {
+        return WARNINGS.getMessages();
+    }
+
+    /**
+     * Provides direct access to the logger used. Use of methods like {@link #warn(String)} is prefered.
+     * @return The logger
+     */
+    public static Logger getLogger() {
+        return LOGGER;
+    }
+
+    private static class RememberWarningHandler extends Handler {
+        private final String[] log = new String[10];
+        private int messagesLogged;
+
+        RememberWarningHandler() {
+            setLevel(LEVEL_WARN);
+        }
+
+        synchronized void clear() {
+            messagesLogged = 0;
+            Arrays.fill(log, null);
+        }
+
+        @Override
+        public synchronized void publish(LogRecord record) {
+            if (!isLoggable(record)) {
+                return;
+            }
+
+            String msg = getPrefix(record) + record.getMessage();
+
+            // Only remember first line of message
+            int idx = msg.indexOf('\n');
+            if (idx > 0) {
+                msg = msg.substring(0, idx);
+            }
+            log[messagesLogged % log.length] = msg;
+            messagesLogged++;
+        }
+
+        private static String getPrefix(LogRecord record) {
+            if (record.getLevel().equals(LEVEL_WARN)) {
+                return "W: ";
+            } else {
+                // worse than warn
+                return "E: ";
+            }
+        }
+
+        synchronized List<String> getMessages() {
+            List<String> logged = Arrays.asList(log);
+            ArrayList<String> res = new ArrayList<>();
+            int logOffset = messagesLogged % log.length;
+            if (messagesLogged > logOffset) {
+                res.addAll(logged.subList(logOffset, log.length));
+            }
+            res.addAll(logged.subList(0, logOffset));
+            return res;
+        }
+
+        @Override
+        public synchronized void flush() {
+            // nothing to do
+        }
+
+        @Override
+        public void close() {
+            // nothing to do
+        }
+    }
+}
diff --git a/src/org/openstreetmap/josm/tools/bugreport/BugReport.java b/src/org/openstreetmap/josm/tools/bugreport/BugReport.java
index d47586f..426cb59 100644
--- a/src/org/openstreetmap/josm/tools/bugreport/BugReport.java
+++ b/src/org/openstreetmap/josm/tools/bugreport/BugReport.java
@@ -5,6 +5,7 @@ import java.io.PrintWriter;
 import java.io.Serializable;
 import java.io.StringWriter;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Predicate;
 
 import org.openstreetmap.josm.actions.ShowStatusReportAction;
 
@@ -180,16 +181,34 @@ public final class BugReport implements Serializable {
      * @return The method name.
      */
     public static String getCallingMethod(int offset) {
-        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
         String className = BugReport.class.getName();
+        String methodName = "getCallingMethod";
+        StackTraceElement found = getCallingMethod(offset, className, methodName::equals);
+        if (found != null) {
+            return found.getClassName().replaceFirst(".*\\.", "") + '#' + found.getMethodName();
+        } else {
+            return "?";
+        }
+    }
+
+    /**
+     * Find the method that called the given method on the current stack trace.
+     * @param offset
+     *           How many methods to look back in the stack trace. 1 gives the method calling this method, 0 gives you getCallingMethod().
+     * @param className The name of the class to search for
+     * @param methodName The name of the method to search for
+     * @return The class and method name or null if it is unknown.
+     */
+    public static StackTraceElement getCallingMethod(int offset, String className, Predicate<String> methodName) {
+        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
         for (int i = 0; i < stackTrace.length - offset; i++) {
             StackTraceElement element = stackTrace[i];
-            if (className.equals(element.getClassName()) && "getCallingMethod".equals(element.getMethodName())) {
+            if (className.equals(element.getClassName()) && methodName.test(element.getMethodName())) {
                 StackTraceElement toReturn = stackTrace[i + offset];
-                return toReturn.getClassName().replaceFirst(".*\\.", "") + '#' + toReturn.getMethodName();
+                return toReturn;
             }
         }
-        return "?";
+        return null;
     }
 
     /**
diff --git a/test/unit/org/openstreetmap/josm/JOSMFixture.java b/test/unit/org/openstreetmap/josm/JOSMFixture.java
index 5e31aa7..7596606 100644
--- a/test/unit/org/openstreetmap/josm/JOSMFixture.java
+++ b/test/unit/org/openstreetmap/josm/JOSMFixture.java
@@ -21,6 +21,7 @@ import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.io.CertificateAmendment;
 import org.openstreetmap.josm.io.OsmApi;
 import org.openstreetmap.josm.tools.I18n;
+import org.openstreetmap.josm.tools.Logging;
 
 /**
  * Fixture to define a proper and safe environment before running tests.
@@ -99,6 +100,7 @@ public class JOSMFixture {
         Main.platform.preStartupHook();
 
         Main.logLevel = 3;
+        Logging.setLogLevel(Logging.LEVEL_INFO);
         Main.pref.init(false);
         Main.pref.put("osm-server.url", "http://api06.dev.openstreetmap.org/api");
         I18n.set(Main.pref.get("language", "en"));
diff --git a/test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java b/test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java
index 634b625..d46b251 100644
--- a/test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java
+++ b/test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java
@@ -5,6 +5,7 @@ import java.io.File;
 import java.io.IOException;
 import java.text.MessageFormat;
 import java.util.TimeZone;
+import java.util.logging.Level;
 
 import org.junit.rules.TemporaryFolder;
 import org.junit.rules.TestRule;
@@ -19,6 +20,7 @@ import org.openstreetmap.josm.io.OsmApi;
 import org.openstreetmap.josm.io.OsmApiInitializationException;
 import org.openstreetmap.josm.io.OsmTransferCanceledException;
 import org.openstreetmap.josm.tools.I18n;
+import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.MemoryManagerTest;
 import org.openstreetmap.josm.tools.date.DateUtils;
 
@@ -182,6 +184,7 @@ public class JOSMTestRules implements TestRule {
         TimeZone.setDefault(DateUtils.UTC);
         // Set log level to info
         Main.logLevel = 3;
+        Logging.setLogLevel(Level.INFO);
 
         // Set up i18n
         if (i18n != null) {
