001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006import gnu.getopt.Getopt;
007import gnu.getopt.LongOpt;
008
009import java.awt.Dimension;
010import java.awt.Image;
011import java.awt.Toolkit;
012import java.awt.event.WindowAdapter;
013import java.awt.event.WindowEvent;
014import java.io.File;
015import java.io.IOException;
016import java.io.InputStream;
017import java.net.Authenticator;
018import java.net.ProxySelector;
019import java.net.URL;
020import java.security.AllPermission;
021import java.security.CodeSource;
022import java.security.KeyStoreException;
023import java.security.NoSuchAlgorithmException;
024import java.security.PermissionCollection;
025import java.security.Permissions;
026import java.security.Policy;
027import java.security.cert.CertificateException;
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.HashMap;
031import java.util.LinkedList;
032import java.util.List;
033import java.util.Map;
034import java.util.Set;
035import java.util.TreeSet;
036
037import javax.swing.JFrame;
038import javax.swing.JOptionPane;
039import javax.swing.RepaintManager;
040import javax.swing.SwingUtilities;
041
042import org.jdesktop.swinghelper.debug.CheckThreadViolationRepaintManager;
043import org.openstreetmap.josm.Main;
044import org.openstreetmap.josm.actions.PreferencesAction;
045import org.openstreetmap.josm.data.AutosaveTask;
046import org.openstreetmap.josm.data.CustomConfigurator;
047import org.openstreetmap.josm.data.Preferences;
048import org.openstreetmap.josm.data.Version;
049import org.openstreetmap.josm.gui.download.DownloadDialog;
050import org.openstreetmap.josm.gui.preferences.server.OAuthAccessTokenHolder;
051import org.openstreetmap.josm.gui.preferences.server.ProxyPreference;
052import org.openstreetmap.josm.gui.progress.ProgressMonitor;
053import org.openstreetmap.josm.gui.util.GuiHelper;
054import org.openstreetmap.josm.io.DefaultProxySelector;
055import org.openstreetmap.josm.io.MessageNotifier;
056import org.openstreetmap.josm.io.auth.CredentialsManager;
057import org.openstreetmap.josm.io.auth.DefaultAuthenticator;
058import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
059import org.openstreetmap.josm.plugins.PluginHandler;
060import org.openstreetmap.josm.plugins.PluginInformation;
061import org.openstreetmap.josm.tools.BugReportExceptionHandler;
062import org.openstreetmap.josm.tools.I18n;
063import org.openstreetmap.josm.tools.ImageProvider;
064import org.openstreetmap.josm.tools.OsmUrlToBounds;
065import org.openstreetmap.josm.tools.PlatformHookWindows;
066import org.openstreetmap.josm.tools.Utils;
067
068/**
069 * Main window class application.
070 *
071 * @author imi
072 */
073public class MainApplication extends Main {
074    /**
075     * Allow subclassing (see JOSM.java)
076     */
077    public MainApplication() {}
078
079    /**
080     * Constructs a main frame, ready sized and operating. Does not display the frame.
081     * @param mainFrame The main JFrame of the application
082     */
083    public MainApplication(JFrame mainFrame) {
084        addListener();
085        mainFrame.setContentPane(contentPanePrivate);
086        mainFrame.setJMenuBar(menu);
087        geometry.applySafe(mainFrame);
088        LinkedList<Image> l = new LinkedList<>();
089        l.add(ImageProvider.get("logo_16x16x32").getImage());
090        l.add(ImageProvider.get("logo_16x16x8").getImage());
091        l.add(ImageProvider.get("logo_32x32x32").getImage());
092        l.add(ImageProvider.get("logo_32x32x8").getImage());
093        l.add(ImageProvider.get("logo_48x48x32").getImage());
094        l.add(ImageProvider.get("logo_48x48x8").getImage());
095        l.add(ImageProvider.get("logo").getImage());
096        mainFrame.setIconImages(l);
097        mainFrame.addWindowListener(new WindowAdapter(){
098            @Override
099            public void windowClosing(final WindowEvent arg0) {
100                Main.exitJosm(true, 0);
101            }
102        });
103        mainFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
104    }
105
106    /**
107     * Displays help on the console
108     * @since 2748
109     */
110    public static void showHelp() {
111        // TODO: put in a platformHook for system that have no console by default
112        System.out.println(tr("Java OpenStreetMap Editor")+" ["
113                +Version.getInstance().getAgentString()+"]\n\n"+
114                tr("usage")+":\n"+
115                "\tjava -jar josm.jar <options>...\n\n"+
116                tr("options")+":\n"+
117                "\t--help|-h                                 "+tr("Show this help")+"\n"+
118                "\t--geometry=widthxheight(+|-)x(+|-)y       "+tr("Standard unix geometry argument")+"\n"+
119                "\t[--download=]minlat,minlon,maxlat,maxlon  "+tr("Download the bounding box")+"\n"+
120                "\t[--download=]<URL>                        "+tr("Download the location at the URL (with lat=x&lon=y&zoom=z)")+"\n"+
121                "\t[--download=]<filename>                   "+tr("Open a file (any file type that can be opened with File/Open)")+"\n"+
122                "\t--downloadgps=minlat,minlon,maxlat,maxlon "+tr("Download the bounding box as raw GPS")+"\n"+
123                "\t--downloadgps=<URL>                       "+tr("Download the location at the URL (with lat=x&lon=y&zoom=z) as raw GPS")+"\n"+
124                "\t--selection=<searchstring>                "+tr("Select with the given search")+"\n"+
125                "\t--[no-]maximize                           "+tr("Launch in maximized mode")+"\n"+
126                "\t--reset-preferences                       "+tr("Reset the preferences to default")+"\n\n"+
127                "\t--load-preferences=<url-to-xml>           "+tr("Changes preferences according to the XML file")+"\n\n"+
128                "\t--set=<key>=<value>                       "+tr("Set preference key to value")+"\n\n"+
129                "\t--language=<language>                     "+tr("Set the language")+"\n\n"+
130                "\t--version                                 "+tr("Displays the JOSM version and exits")+"\n\n"+
131                "\t--debug                                   "+tr("Print debugging messages to console")+"\n\n"+
132                tr("options provided as Java system properties")+":\n"+
133                "\t-Djosm.home="+tr("/PATH/TO/JOSM/FOLDER/         ")+tr("Change the folder for all user settings")+"\n\n"+
134                tr("note: For some tasks, JOSM needs a lot of memory. It can be necessary to add the following\n" +
135                        "      Java option to specify the maximum size of allocated memory in megabytes")+":\n"+
136                        "\t-Xmx...m\n\n"+
137                        tr("examples")+":\n"+
138                        "\tjava -jar josm.jar track1.gpx track2.gpx london.osm\n"+
139                        "\tjava -jar josm.jar "+OsmUrlToBounds.getURL(43.2, 11.1, 13)+"\n"+
140                        "\tjava -jar josm.jar london.osm --selection=http://www.ostertag.name/osm/OSM_errors_node-duplicate.xml\n"+
141                        "\tjava -jar josm.jar 43.2,11.1,43.4,11.4\n"+
142                        "\tjava -Djosm.home=/home/user/.josm_dev -jar josm.jar\n"+
143                        "\tjava -Xmx1024m -jar josm.jar\n\n"+
144                        tr("Parameters --download, --downloadgps, and --selection are processed in this order.")+"\n"+
145                        tr("Make sure you load some data if you use --selection.")+"\n"
146                );
147    }
148
149    /**
150     * JOSM command line options.
151     * @see <a href="https://josm.openstreetmap.de/wiki/Help/CommandLineOptions">Help/CommandLineOptions</a>
152     * @since 5279
153     */
154    public enum Option {
155        /** --help|-h                                 Show this help */
156        HELP(false),
157        /** --version                                 Displays the JOSM version and exits */
158        VERSION(false),
159        /** --debug                                   Print debugging messages to console */
160        DEBUG(false),
161        /** --trace                                   Print detailed debugging messages to console */
162        TRACE(false),
163        /** --language=&lt;language&gt;               Set the language */
164        LANGUAGE(true),
165        /** --reset-preferences                       Reset the preferences to default */
166        RESET_PREFERENCES(false),
167        /** --load-preferences=&lt;url-to-xml&gt;     Changes preferences according to the XML file */
168        LOAD_PREFERENCES(true),
169        /** --set=&lt;key&gt;=&lt;value&gt;           Set preference key to value */
170        SET(true),
171        /** --geometry=widthxheight(+|-)x(+|-)y       Standard unix geometry argument */
172        GEOMETRY(true),
173        /** --no-maximize                             Do not launch in maximized mode */
174        NO_MAXIMIZE(false),
175        /** --maximize                                Launch in maximized mode */
176        MAXIMIZE(false),
177        /** --download=minlat,minlon,maxlat,maxlon    Download the bounding box <br>
178         *  --download=&lt;URL&gt;                    Download the location at the URL (with lat=x&amp;lon=y&amp;zoom=z) <br>
179         *  --download=&lt;filename&gt;               Open a file (any file type that can be opened with File/Open) */
180        DOWNLOAD(true),
181        /** --downloadgps=minlat,minlon,maxlat,maxlon Download the bounding box as raw GPS <br>
182         *  --downloadgps=&lt;URL&gt;                 Download the location at the URL (with lat=x&amp;lon=y&amp;zoom=z) as raw GPS */
183        DOWNLOADGPS(true),
184        /** --selection=&lt;searchstring&gt;          Select with the given search */
185        SELECTION(true);
186
187        private String name;
188        private boolean requiresArgument;
189
190        private Option(boolean requiresArgument) {
191            this.name = name().toLowerCase().replace("_", "-");
192            this.requiresArgument = requiresArgument;
193        }
194
195        /**
196         * Replies the option name
197         * @return The option name, in lowercase
198         */
199        public String getName() {
200            return name;
201        }
202
203        /**
204         * Determines if this option requires an argument.
205         * @return {@code true} if this option requires an argument, {@code false} otherwise
206         */
207        public boolean requiresArgument() {
208            return requiresArgument;
209        }
210
211        public static Map<Option, Collection<String>> fromStringMap(Map<String, Collection<String>> opts) {
212            Map<Option, Collection<String>> res = new HashMap<>();
213            for (Map.Entry<String, Collection<String>> e : opts.entrySet()) {
214                Option o = Option.valueOf(e.getKey().toUpperCase().replace("-", "_"));
215                if (o != null) {
216                    res.put(o, e.getValue());
217                }
218            }
219            return res;
220        }
221    }
222
223    private static Map<Option, Collection<String>> buildCommandLineArgumentMap(String[] args) {
224
225        List<LongOpt> los = new ArrayList<>();
226        for (Option o : Option.values()) {
227            los.add(new LongOpt(o.getName(), o.requiresArgument() ? LongOpt.REQUIRED_ARGUMENT : LongOpt.NO_ARGUMENT, null, 0));
228        }
229
230        Getopt g = new Getopt("JOSM", args, "hv", los.toArray(new LongOpt[los.size()]));
231
232        Map<Option, Collection<String>> argMap = new HashMap<>();
233
234        int c;
235        while ((c = g.getopt()) != -1 ) {
236            Option opt = null;
237            switch (c) {
238                case 'h':
239                    opt = Option.HELP;
240                    break;
241                case 'v':
242                    opt = Option.VERSION;
243                    break;
244                case 0:
245                    opt = Option.values()[g.getLongind()];
246                    break;
247            }
248            if (opt != null) {
249                Collection<String> values = argMap.get(opt);
250                if (values == null) {
251                    values = new ArrayList<>();
252                    argMap.put(opt, values);
253                }
254                values.add(g.getOptarg());
255            } else
256                throw new IllegalArgumentException();
257        }
258        // positional arguments are a shortcut for the --download ... option
259        for (int i = g.getOptind(); i < args.length; ++i) {
260            Collection<String> values = argMap.get(Option.DOWNLOAD);
261            if (values == null) {
262                values = new ArrayList<>();
263                argMap.put(Option.DOWNLOAD, values);
264            }
265            values.add(args[i]);
266        }
267
268        return argMap;
269    }
270
271    /**
272     * Main application Startup
273     * @param argArray Command-line arguments
274     */
275    public static void main(final String[] argArray) {
276        I18n.init();
277        Main.checkJavaVersion();
278
279        // construct argument table
280        Map<Option, Collection<String>> args = null;
281        try {
282            args = buildCommandLineArgumentMap(argArray);
283        } catch (IllegalArgumentException e) {
284            System.exit(1);
285        }
286
287        final boolean languageGiven = args.containsKey(Option.LANGUAGE);
288
289        if (languageGiven) {
290            I18n.set(args.get(Option.LANGUAGE).iterator().next());
291        }
292
293        initApplicationPreferences();
294
295        Policy.setPolicy(new Policy() {
296            // Permissions for plug-ins loaded when josm is started via webstart
297            private PermissionCollection pc;
298
299            {
300                pc = new Permissions();
301                pc.add(new AllPermission());
302            }
303
304            @Override
305            public void refresh() { }
306
307            @Override
308            public PermissionCollection getPermissions(CodeSource codesource) {
309                return pc;
310            }
311        });
312
313        Thread.setDefaultUncaughtExceptionHandler(new BugReportExceptionHandler());
314
315        // initialize the platform hook, and
316        Main.determinePlatformHook();
317        // call the really early hook before we do anything else
318        Main.platform.preStartupHook();
319
320        Main.commandLineArgs = Utils.copyArray(argArray);
321
322        if (args.containsKey(Option.VERSION)) {
323            System.out.println(Version.getInstance().getAgentString());
324            System.exit(0);
325        }
326
327        if (args.containsKey(Option.DEBUG) || args.containsKey(Option.TRACE)) {
328            // Enable JOSM debug level
329            logLevel = 4;
330            Main.info(tr("Printing debugging messages to console"));
331        }
332
333        if (args.containsKey(Option.TRACE)) {
334            // Enable JOSM debug level
335            logLevel = 5;
336            // Enable debug in OAuth signpost via system preference, but only at trace level
337            Preferences.updateSystemProperty("debug", "true");
338            Main.info(tr("Enabled detailed debug level (trace)"));
339        }
340
341        Main.pref.init(args.containsKey(Option.RESET_PREFERENCES));
342
343        if (!languageGiven) {
344            I18n.set(Main.pref.get("language", null));
345        }
346        Main.pref.updateSystemProperties();
347
348        final JFrame mainFrame = new JFrame(tr("Java OpenStreetMap Editor"));
349        Main.parent = mainFrame;
350
351        if (args.containsKey(Option.LOAD_PREFERENCES)) {
352            CustomConfigurator.XMLCommandProcessor config = new CustomConfigurator.XMLCommandProcessor(Main.pref);
353            for (String i : args.get(Option.LOAD_PREFERENCES)) {
354                info("Reading preferences from " + i);
355                try (InputStream is = Utils.openURL(new URL(i))) {
356                    config.openAndReadXML(is);
357                } catch (Exception ex) {
358                    throw new RuntimeException(ex);
359                }
360            }
361        }
362
363        if (args.containsKey(Option.SET)) {
364            for (String i : args.get(Option.SET)) {
365                String[] kv = i.split("=", 2);
366                Main.pref.put(kv[0], "null".equals(kv[1]) ? null : kv[1]);
367            }
368        }
369
370        DefaultAuthenticator.createInstance();
371        Authenticator.setDefault(DefaultAuthenticator.getInstance());
372        DefaultProxySelector proxySelector = new DefaultProxySelector(ProxySelector.getDefault());
373        ProxySelector.setDefault(proxySelector);
374        OAuthAccessTokenHolder.getInstance().init(Main.pref, CredentialsManager.getInstance());
375
376        // asking for help? show help and exit
377        if (args.containsKey(Option.HELP)) {
378            showHelp();
379            System.exit(0);
380        }
381
382        final SplashScreen splash = new SplashScreen();
383        final ProgressMonitor monitor = splash.getProgressMonitor();
384        monitor.beginTask(tr("Initializing"));
385        splash.setVisible(Main.pref.getBoolean("draw.splashscreen", true));
386        Main.setInitStatusListener(new InitStatusListener() {
387
388            @Override
389            public void updateStatus(String event) {
390                monitor.indeterminateSubTask(event);
391            }
392        });
393
394        Collection<PluginInformation> pluginsToLoad = PluginHandler.buildListOfPluginsToLoad(splash,monitor.createSubTaskMonitor(1, false));
395        if (!pluginsToLoad.isEmpty() && PluginHandler.checkAndConfirmPluginUpdate(splash)) {
396            monitor.subTask(tr("Updating plugins"));
397            pluginsToLoad = PluginHandler.updatePlugins(splash, null, monitor.createSubTaskMonitor(1, false), false);
398        }
399
400        monitor.indeterminateSubTask(tr("Installing updated plugins"));
401        PluginHandler.installDownloadedPlugins(true);
402
403        monitor.indeterminateSubTask(tr("Loading early plugins"));
404        PluginHandler.loadEarlyPlugins(splash,pluginsToLoad, monitor.createSubTaskMonitor(1, false));
405
406        monitor.indeterminateSubTask(tr("Setting defaults"));
407        preConstructorInit(args);
408
409        monitor.indeterminateSubTask(tr("Creating main GUI"));
410        final Main main = new MainApplication(mainFrame);
411
412        monitor.indeterminateSubTask(tr("Loading plugins"));
413        PluginHandler.loadLatePlugins(splash,pluginsToLoad,  monitor.createSubTaskMonitor(1, false));
414        toolbar.refreshToolbarControl();
415
416        // Wait for splash disappearance (fix #9714)
417        GuiHelper.runInEDTAndWait(new Runnable() {
418            @Override
419            public void run() {
420                splash.setVisible(false);
421                splash.dispose();
422                mainFrame.setVisible(true);
423            }
424        });
425
426        Main.MasterWindowListener.setup();
427
428        boolean maximized = Main.pref.getBoolean("gui.maximized", false);
429        if ((!args.containsKey(Option.NO_MAXIMIZE) && maximized) || args.containsKey(Option.MAXIMIZE)) {
430            if (Toolkit.getDefaultToolkit().isFrameStateSupported(JFrame.MAXIMIZED_BOTH)) {
431                Main.windowState = JFrame.MAXIMIZED_BOTH;
432                mainFrame.setExtendedState(Main.windowState);
433            } else {
434                Main.debug("Main window: maximizing not supported");
435            }
436        }
437        if (main.menu.fullscreenToggleAction != null) {
438            main.menu.fullscreenToggleAction.initial();
439        }
440
441        SwingUtilities.invokeLater(new GuiFinalizationWorker(args, proxySelector));
442
443        if (Main.isPlatformWindows()) {
444            try {
445                // Check for insecure certificates to remove.
446                // This is Windows-dependant code but it can't go to preStartupHook (need i18n) neither startupHook (need to be called before remote control)
447                PlatformHookWindows.removeInsecureCertificates();
448            } catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException e) {
449                error(e);
450            }
451        }
452
453        if (RemoteControl.PROP_REMOTECONTROL_ENABLED.get()) {
454            RemoteControl.start();
455        }
456
457        if (MessageNotifier.PROP_NOTIFIER_ENABLED.get()) {
458            MessageNotifier.start();
459        }
460
461        if (Main.pref.getBoolean("debug.edt-checker.enable", Version.getInstance().isLocalBuild())) {
462            // Repaint manager is registered so late for a reason - there is lots of violation during startup process but they don't seem to break anything and are difficult to fix
463            info("Enabled EDT checker, wrongful access to gui from non EDT thread will be printed to console");
464            RepaintManager.setCurrentManager(new CheckThreadViolationRepaintManager());
465        }
466    }
467
468    private static class GuiFinalizationWorker implements Runnable {
469
470        private final Map<Option, Collection<String>> args;
471        private final DefaultProxySelector proxySelector;
472
473        public GuiFinalizationWorker(Map<Option, Collection<String>> args, DefaultProxySelector proxySelector) {
474            this.args = args;
475            this.proxySelector = proxySelector;
476        }
477
478        @Override
479        public void run() {
480
481            // Handle proxy/network errors early to inform user he should change settings to be able to use JOSM correctly
482            if (!handleProxyErrors()) {
483                handleNetworkErrors();
484            }
485
486            // Restore autosave layers after crash and start autosave thread
487            handleAutosave();
488
489            // Handle command line instructions
490            postConstructorProcessCmdLine(args);
491
492            // Show download dialog if autostart is enabled
493            DownloadDialog.autostartIfNeeded();
494        }
495
496        private void handleAutosave() {
497            if (AutosaveTask.PROP_AUTOSAVE_ENABLED.get()) {
498                AutosaveTask autosaveTask = new AutosaveTask();
499                List<File> unsavedLayerFiles = autosaveTask.getUnsavedLayersFiles();
500                if (!unsavedLayerFiles.isEmpty()) {
501                    ExtendedDialog dialog = new ExtendedDialog(
502                            Main.parent,
503                            tr("Unsaved osm data"),
504                            new String[] {tr("Restore"), tr("Cancel"), tr("Discard")}
505                            );
506                    dialog.setContent(
507                            trn("JOSM found {0} unsaved osm data layer. ",
508                                    "JOSM found {0} unsaved osm data layers. ", unsavedLayerFiles.size(), unsavedLayerFiles.size()) +
509                                    tr("It looks like JOSM crashed last time. Would you like to restore the data?"));
510                    dialog.setButtonIcons(new String[] {"ok", "cancel", "dialogs/delete"});
511                    int selection = dialog.showDialog().getValue();
512                    if (selection == 1) {
513                        autosaveTask.recoverUnsavedLayers();
514                    } else if (selection == 3) {
515                        autosaveTask.discardUnsavedLayers();
516                    }
517                }
518                autosaveTask.schedule();
519            }
520        }
521
522        private boolean handleNetworkOrProxyErrors(boolean hasErrors, String title, String message) {
523            if (hasErrors) {
524                ExtendedDialog ed = new ExtendedDialog(
525                        Main.parent, title,
526                        new String[]{tr("Change proxy settings"), tr("Cancel")});
527                ed.setButtonIcons(new String[]{"dialogs/settings.png", "cancel.png"}).setCancelButton(2);
528                ed.setMinimumSize(new Dimension(460, 260));
529                ed.setIcon(JOptionPane.WARNING_MESSAGE);
530                ed.setContent(message);
531
532                if (ed.showDialog().getValue() == 1) {
533                    PreferencesAction.forPreferenceSubTab(null, null, ProxyPreference.class).run();
534                }
535            }
536            return hasErrors;
537        }
538
539        private boolean handleProxyErrors() {
540            return handleNetworkOrProxyErrors(proxySelector.hasErrors(), tr("Proxy errors occurred"),
541                    tr("JOSM tried to access the following resources:<br>" +
542                            "{0}" +
543                            "but <b>failed</b> to do so, because of the following proxy errors:<br>" +
544                            "{1}" +
545                            "Would you like to change your proxy settings now?",
546                            Utils.joinAsHtmlUnorderedList(proxySelector.getErrorResources()),
547                            Utils.joinAsHtmlUnorderedList(proxySelector.getErrorMessages())
548                    ));
549        }
550
551        private boolean handleNetworkErrors() {
552            boolean condition = !NETWORK_ERRORS.isEmpty();
553            if (condition) {
554                Set<String> errors = new TreeSet<>();
555                for (Throwable t : NETWORK_ERRORS.values()) {
556                    errors.add(t.toString());
557                }
558                return handleNetworkOrProxyErrors(condition, tr("Network errors occurred"),
559                        tr("JOSM tried to access the following resources:<br>" +
560                                "{0}" +
561                                "but <b>failed</b> to do so, because of the following network errors:<br>" +
562                                "{1}" +
563                                "It may be due to a missing proxy configuration.<br>" +
564                                "Would you like to change your proxy settings now?",
565                                Utils.joinAsHtmlUnorderedList(NETWORK_ERRORS.keySet()),
566                                Utils.joinAsHtmlUnorderedList(errors)
567                        ));
568            }
569            return false;
570        }
571    }
572}