001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2023
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * http://www.ssec.wisc.edu/mcidas
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see http://www.gnu.org/licenses.
027 */
028
029package edu.wisc.ssec.mcidasv;
030
031import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
032import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.cast;
033import static ucar.unidata.xml.XmlUtil.getAttribute;
034
035import java.awt.Insets;
036import java.awt.geom.Rectangle2D;
037
038import java.io.BufferedReader;
039import java.io.File;
040import java.io.FileOutputStream;
041import java.io.FileReader;
042import java.io.IOException;
043import java.io.PrintStream;
044
045import java.lang.reflect.Method;
046import java.net.URL;
047import java.net.URLConnection;
048import java.net.URLStreamHandler;
049
050import java.security.Security;
051import java.util.Collections;
052import java.util.Date;
053import java.util.EnumSet;
054import java.util.HashMap;
055import java.util.Hashtable;
056import java.util.LinkedList;
057import java.util.List;
058import java.util.Map;
059import java.util.Objects;
060import java.util.Properties;
061import java.util.Set;
062
063import javax.swing.Icon;
064import javax.swing.JButton;
065import javax.swing.JCheckBox;
066import javax.swing.JComponent;
067import javax.swing.JDialog;
068import javax.swing.JLabel;
069import javax.swing.JOptionPane;
070import javax.swing.SwingUtilities;
071import javax.swing.ToolTipManager;
072
073import com.formdev.flatlaf.FlatDarkLaf;
074import edu.wisc.ssec.mcidas.adde.AddeURL;
075import edu.wisc.ssec.mcidas.adde.AddeURLStreamHandler;
076
077import com.google.common.base.Strings;
078import edu.wisc.ssec.mcidasv.collaboration.CollaborationManager;
079import edu.wisc.ssec.mcidasv.ui.McIDASVXmlUi;
080import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
081import edu.wisc.ssec.mcidasv.util.OptionPaneClicker;
082import edu.wisc.ssec.mcidasv.util.SystemState;
083import edu.wisc.ssec.mcidasv.util.WebBrowser;
084import edu.wisc.ssec.mcidasv.util.WelcomeWindow;
085
086import javafx.application.Platform;
087import javafx.embed.swing.JFXPanel;
088import org.joda.time.DateTime;
089import org.python.util.PythonInterpreter;
090import org.w3c.dom.Element;
091
092import ucar.nc2.NetcdfFile;
093import visad.VisADException;
094
095import ucar.unidata.data.DataManager;
096import ucar.unidata.idv.ArgsManager;
097import ucar.unidata.idv.ControlDescriptor;
098import ucar.unidata.idv.IdvObjectStore;
099import ucar.unidata.idv.IdvPersistenceManager;
100import ucar.unidata.idv.IdvPreferenceManager;
101import ucar.unidata.idv.IdvResourceManager;
102import ucar.unidata.idv.IntegratedDataViewer;
103import ucar.unidata.idv.PluginManager;
104import ucar.unidata.idv.VMManager;
105import ucar.unidata.idv.ViewDescriptor;
106import ucar.unidata.idv.ViewManager;
107import ucar.unidata.idv.chooser.IdvChooserManager;
108import ucar.unidata.idv.collab.CollabManager;
109import ucar.unidata.idv.ui.IdvUIManager;
110import ucar.unidata.idv.ui.IdvWindow;
111import ucar.unidata.ui.colortable.ColorTableManager;
112import ucar.unidata.ui.InteractiveShell.ShellHistoryEntry;
113import ucar.unidata.util.FileManager;
114import ucar.unidata.util.GuiUtils;
115import ucar.unidata.util.IOUtil;
116import ucar.unidata.util.LogUtil;
117import ucar.unidata.util.Misc;
118import ucar.unidata.xml.XmlDelegateImpl;
119import ucar.unidata.xml.XmlEncoder;
120import ucar.unidata.xml.XmlUtil;
121
122import org.bushe.swing.event.EventBus;
123import org.bushe.swing.event.annotation.AnnotationProcessor;
124import org.bushe.swing.event.annotation.EventSubscriber;
125
126import org.slf4j.bridge.SLF4JBridgeHandler;
127import org.slf4j.Logger;
128import org.slf4j.LoggerFactory;
129
130import uk.org.lidalia.sysoutslf4j.context.LogLevel;
131import uk.org.lidalia.sysoutslf4j.context.SysOutOverSLF4J;
132
133import edu.wisc.ssec.mcidasv.data.GpmIosp;
134import edu.wisc.ssec.mcidasv.data.TropomiIOSP;
135import edu.wisc.ssec.mcidasv.chooser.McIdasChooserManager;
136import edu.wisc.ssec.mcidasv.control.LambertAEA;
137import edu.wisc.ssec.mcidasv.data.McvDataManager;
138import edu.wisc.ssec.mcidasv.monitors.MonitorManager;
139import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource;
140import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
141import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
142import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity;
143import edu.wisc.ssec.mcidasv.servermanager.AddePreferences;
144import edu.wisc.ssec.mcidasv.servermanager.EntryStore;
145import edu.wisc.ssec.mcidasv.servermanager.EntryTransforms;
146import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry;
147import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry.AddeFormat;
148import edu.wisc.ssec.mcidasv.servermanager.RemoteAddeEntry;
149import edu.wisc.ssec.mcidasv.servermanager.TabbedAddeManager;
150import edu.wisc.ssec.mcidasv.startupmanager.StartupManager;
151import edu.wisc.ssec.mcidasv.ui.LayerAnimationWindow;
152import edu.wisc.ssec.mcidasv.ui.McIdasColorTableManager;
153import edu.wisc.ssec.mcidasv.ui.UIManager;
154import edu.wisc.ssec.mcidasv.util.gui.EventDispatchThreadHangMonitor;
155import edu.wisc.ssec.mcidasv.util.pathwatcher.DirectoryWatchService;
156import edu.wisc.ssec.mcidasv.util.pathwatcher.OnFileChangeListener;
157import edu.wisc.ssec.mcidasv.util.pathwatcher.SimpleDirectoryWatchService;
158
159/**
160 * Class used as the {@literal "gateway"} to a running McIDAS-V session.
161 *
162 * <p>This is where the startup and shutdown processes are handled, as well as
163 * the initialization of the application's various {@literal "managers"}.</p>
164 */
165@SuppressWarnings("unchecked")
166public class McIDASV extends IntegratedDataViewer {
167    
168    /** Logging object. */
169    private static final Logger logger =
170        LoggerFactory.getLogger(McIDASV.class);
171        
172    /**
173     * Initialization start time. This value is set at the beginning of
174     * {@link #main(String[])}, and is used to estimate the duration of the
175     * session's initialization phase.
176     */
177    private static long startTime;
178    
179    /**
180     * Initialization duration. Set at the end of {@link #initDone()}.
181     */
182    private static long estimate;
183    
184    /** 
185     * Path to a {@literal "session"} file--it's created upon McIDAS-V 
186     * starting and removed when McIDAS-V exits cleanly. This allows us to
187     * perform a primitive check to see if the current session has happened
188     * after a crash. 
189     */
190    private static String SESSION_FILE = getSessionFilePath();
191    
192    /**
193     * Whether or not the previous session was able to exit as it should.
194     * If {@code false}, the previous session likely crashed.
195     */
196    private static boolean cleanExit = true;
197    
198    /** Date the previous session was started. May be {@code null}. */
199    private static Date previousStart = null;
200    
201    /** Set to true only if "-forceaqua" was found in the command line. */
202    public static boolean useAquaLookAndFeel = false;
203    
204    /** Points to the adde image defaults. */
205    public static final IdvResourceManager.XmlIdvResource RSC_FRAMEDEFAULTS =
206        new IdvResourceManager.XmlIdvResource("idv.resource.framedefaults",
207                           "McIDAS-X Frame Defaults");
208                           
209    /** Points to the server definitions. */
210    public static final IdvResourceManager.XmlIdvResource RSC_SERVERS =
211        new IdvResourceManager.XmlIdvResource("idv.resource.servers",
212                           "Servers", "servers\\.xml$");
213                           
214    /** Used to access McIDAS-V state in a static context. */
215    private static McIDASV staticMcv;
216    
217    /** Accessory in file save dialog */
218    private JCheckBox overwriteDataCbx = 
219        new JCheckBox("Change data paths", false);
220    
221    /** Chooser manager */
222    protected McIdasChooserManager chooserManager;
223    
224    /** HTTP based monitor to dump stack traces and shutdown McIDAS-V. */
225    private McIDASVMonitor mcvMonitor;
226    
227    /**
228     * {@link MonitorManager} allows for relatively easy and efficient
229     * monitoring of various resources.
230     */
231    private final MonitorManager monitorManager = new MonitorManager();
232    
233    /**
234     * Actions passed into {@link #handleAction(String, Hashtable, boolean)}.
235     */
236    private final List<String> actions = new LinkedList<>();
237    
238    private enum WarningResult { OK, CANCEL, SHOW, HIDE };
239    
240    /** Reference to the ADDE server manager. */
241    private EntryStore addeEntries;
242    
243    /**
244     * GUI wrapper for ADDE server management. Reference is kept due to some
245     * of the trickery used by {@link AddePreferences}.
246     *
247     * <p>Value may be {@code null}.</p>
248     */
249    private TabbedAddeManager tabbedAddeManager = null;
250    
251    /** Directory monitoring service. */
252    private final DirectoryWatchService watchService;
253    
254    /**
255     * Create the McIDASV with the given command line arguments.
256     * This constructor calls {@link IntegratedDataViewer#init()}
257     * 
258     * @param args Command line arguments
259     * @exception VisADException  from construction of VisAd objects
260     * @exception RemoteException from construction of VisAD objects
261     */
262    public McIDASV(String[] args) throws IOException, VisADException {
263        super(args);
264        
265        AnnotationProcessor.process(this);
266        
267        staticMcv = this;
268        
269        // Set up our application to respond to the Mac OS X application menu
270        registerForMacOSXEvents();
271        
272        // we're tired of the IDV's default missing image, so reset it
273        GuiUtils.MISSING_IMAGE =
274            "/edu/wisc/ssec/mcidasv/resources/icons/toolbar/mcidasv-round22.png";
275        
276        watchService = new SimpleDirectoryWatchService();
277        
278        this.init();
279        
280        // ensure jython init only happens once per application session
281        String cacheDir = getStore().getJythonCacheDir();
282        Properties pyProps = new Properties();
283        if (cacheDir != null) {
284            pyProps.put("python.home", cacheDir);
285        }
286        String[] blank = new String[] { "" };
287        PythonInterpreter.initialize(System.getProperties(), pyProps, blank);
288    }
289    
290    /**
291     * Generic registration with the macOS application menu.
292     *
293     * <p>Checks the platform, then attempts to register with Apple's
294     * {@literal "EAWT"} stuff.</p>
295     *
296     * <p>See {@code OSXAdapter.java} to learn how this is done without
297     * directly referencing any Apple APIs.</p>
298     */
299    public void registerForMacOSXEvents() {
300        // TODO(jon): remove?
301        if (isMac()) {
302            try {
303                // Generate and register the OSXAdapter, passing it a hash of all the methods we wish to
304                // use as delegates for various com.apple.eawt.ApplicationListener methods
305                Class<?> thisClass = getClass();
306                Class<?>[] args = (Class[])null;
307                OSXAdapter.setQuitHandler(this, thisClass.getDeclaredMethod("MacOSXQuit", args));
308                OSXAdapter.setAboutHandler(this, thisClass.getDeclaredMethod("MacOSXAbout", args));
309                OSXAdapter.setPreferencesHandler(this, thisClass.getDeclaredMethod("MacOSXPreferences", args));
310            } catch (Exception e) {
311                logger.error("Error while loading the OSXAdapter", e);
312            }
313        }
314    }
315    
316    public boolean MacOSXQuit() {
317        // TODO(jon): remove?
318        return quit();
319    }
320    
321    public void MacOSXAbout() {
322        // TODO(jon): remove?
323        getIdvUIManager().about();
324    }
325    
326    public void MacOSXPreferences() {
327        // TODO(jon): remove?
328        showPreferenceManager();
329    }
330    
331    /**
332     * Get the maximum number of threads to be used when rendering in VisAD.
333     *
334     * @return Number of threads for rendering. Default value is the same as
335     * {@link Runtime#availableProcessors()}.
336     */
337    @Override public int getMaxRenderThreadCount() {
338        StateManager stateManager = (StateManager)getStateManager();
339        return stateManager.getPropertyOrPreference(PREF_THREADS_RENDER,
340            Runtime.getRuntime().availableProcessors());
341    }
342    
343    /**
344     * Get the maximum number of threads to be used when reading data.
345     *
346     * @return Number of threads for reading data. Default value is {@code 4}.
347     */
348    @Override public int getMaxDataThreadCount() {
349        StateManager stateManager = (StateManager)getStateManager();
350        return stateManager.getPropertyOrPreference(PREF_THREADS_DATA, 4);
351    }
352    
353    /**
354     * Start up the McIDAS-V monitor server.
355     *
356     * <p>This is an HTTP server on the port defined by the property
357     * {@code idv.monitorport}. Default value is 8788.</p>
358     *
359     * <p>It is only accessible to 127.0.0.1 (localhost).</p>
360     */
361    @Override protected void startMonitor() {
362        if (mcvMonitor != null) {
363            return;
364        }
365        final String monitorPort = getProperty(PROP_MONITORPORT, "");
366        if (monitorPort!=null && monitorPort.trim().length()>0 && !"none".equals(monitorPort.trim())) {
367            Misc.run(() -> {
368                try {
369                    mcvMonitor =
370                        new McIDASVMonitor(McIDASV.this,
371                                           Integer.parseInt(monitorPort));
372                    mcvMonitor.init();
373                } catch (Exception exc) {
374                    LogUtil.consoleMessage("Unable to start McIDAS-V monitor on port:" + monitorPort);
375                    LogUtil.consoleMessage("Error:" + exc);
376                }
377            });
378        }
379    }
380    
381    /**
382     * Initializes a XML encoder with McIDAS-V specific XML delegates.
383     * 
384     * @param encoder XML encoder that'll be dealing with persistence.
385     * @param forRead Not used as of yet.
386     */
387    // TODO: if we ever get up past three or so XML delegates, I vote that we
388    // make our own version of VisADPersistence.
389    @Override protected void initEncoder(XmlEncoder encoder, boolean forRead) {
390        
391        encoder.addDelegateForClass(LambertAEA.class, new XmlDelegateImpl() {
392            @Override public Element createElement(XmlEncoder e, Object o) {
393                LambertAEA projection = (LambertAEA)o;
394                Rectangle2D rect = projection.getDefaultMapArea();
395                List args = Misc.newList(rect);
396                List types = Misc.newList(rect.getClass());
397                return e.createObjectConstructorElement(o, args, types);
398            }
399        });
400        
401        encoder.addDelegateForClass(ShellHistoryEntry.class, new XmlDelegateImpl() {
402            @Override public Element createElement(XmlEncoder e, Object o) {
403                ShellHistoryEntry entry = (ShellHistoryEntry)o;
404                List args = Misc.newList(entry.getEntryBytes(), entry.getInputMode().toString());
405                return e.createObjectConstructorElement(o, args);
406            }
407        });
408        
409        // TODO(jon): ultra fashion makeover!!
410        encoder.addDelegateForClass(RemoteAddeEntry.class, new XmlDelegateImpl() {
411            @Override public Element createElement(XmlEncoder e, Object o) {
412                RemoteAddeEntry entry = (RemoteAddeEntry)o;
413                Element element = e.createObjectElement(o.getClass());
414                element.setAttribute("address", entry.getAddress());
415                element.setAttribute("group", entry.getGroup());
416                element.setAttribute("username", entry.getAccount().getUsername());
417                element.setAttribute("project", entry.getAccount().getProject());
418                element.setAttribute("source", entry.getEntrySource().toString());
419                element.setAttribute("type", entry.getEntryType().toString());
420                element.setAttribute("validity", entry.getEntryValidity().toString());
421                element.setAttribute("status", entry.getEntryStatus().toString());
422                element.setAttribute("temporary", Boolean.toString(entry.isEntryTemporary()));
423                element.setAttribute("alias", entry.getEntryAlias());
424                return element;
425            }
426            
427            @Override public Object createObject(XmlEncoder e, Element element) {
428                String address = getAttribute(element, "address");
429                String group = getAttribute(element, "group");
430                String username = getAttribute(element, "username");
431                String project = getAttribute(element, "project");
432                String source = getAttribute(element, "source");
433                String type = getAttribute(element, "type");
434                String validity = getAttribute(element, "validity");
435                String status = getAttribute(element, "status");
436                boolean temporary = getAttribute(element, "temporary", false);
437                String alias = getAttribute(element, "alias", "");
438                
439                EntrySource entrySource = EntryTransforms.strToEntrySource(source);
440                EntryType entryType = EntryTransforms.strToEntryType(type);
441                EntryValidity entryValidity = EntryTransforms.strToEntryValidity(validity);
442                EntryStatus entryStatus = EntryTransforms.strToEntryStatus(status);
443                
444                return new RemoteAddeEntry.Builder(address, group)
445                                          .account(username, project)
446                                          .source(entrySource)
447                                          .type(entryType)
448                                          .validity(entryValidity)
449                                          .temporary(temporary)
450                                          .alias(alias)
451                                          .status(entryStatus).build();
452            }
453        });
454        
455        encoder.addDelegateForClass(LocalAddeEntry.class, new XmlDelegateImpl() {
456            @Override public Element createElement(XmlEncoder e, Object o) {
457                LocalAddeEntry entry = (LocalAddeEntry)o;
458                Element element = e.createObjectElement(o.getClass());
459                element.setAttribute("group", entry.getGroup());
460                element.setAttribute("descriptor", entry.getDescriptor());
461                element.setAttribute("realtime", entry.getRealtimeAsString());
462                element.setAttribute("format", entry.getFormat().name());
463                element.setAttribute("start", entry.getStart());
464                element.setAttribute("end", entry.getEnd());
465                element.setAttribute("fileMask", entry.getMask());
466                element.setAttribute("name", entry.getName());
467                element.setAttribute("status", entry.getEntryStatus().name());
468                element.setAttribute("temporary", Boolean.toString(entry.isEntryTemporary()));
469                element.setAttribute("alias", entry.getEntryAlias());
470                return element;
471            }
472            
473            @Override public Object createObject(XmlEncoder e, Element element) {
474                String group = getAttribute(element, "group");
475                String descriptor = getAttribute(element, "descriptor");
476                String realtime = getAttribute(element, "realtime", "");
477                AddeFormat format = EntryTransforms.strToAddeFormat(XmlUtil.getAttribute(element, "format"));
478                String start = getAttribute(element, "start", "1");
479                String end = getAttribute(element, "end", "999999");
480                String fileMask = getAttribute(element, "fileMask");
481                String name = getAttribute(element, "name");
482                String status = getAttribute(element, "status", "ENABLED");
483                boolean temporary = getAttribute(element, "temporary", false);
484                String alias = getAttribute(element, "alias", "");
485                
486                return new LocalAddeEntry.Builder(name, group, fileMask, format)
487                                         .range(start, end)
488                                         .descriptor(descriptor)
489                                         .realtime(realtime)
490                                         .status(status)
491                                         .temporary(temporary)
492                                         .alias(alias).build();
493            }
494        });
495        
496        // Move legacy classes to a new location
497        encoder.registerNewClassName(
498            "edu.wisc.ssec.mcidasv.data.Test2ImageDataSource",
499            "edu.wisc.ssec.mcidasv.data.adde.AddeImageParameterDataSource");
500        encoder.registerNewClassName(
501            "edu.wisc.ssec.mcidasv.data.Test2AddeImageDataSource",
502            "edu.wisc.ssec.mcidasv.data.adde.AddeImageParameterDataSource");
503        encoder.registerNewClassName(
504            "edu.wisc.ssec.mcidasv.data.AddePointDataSource",
505            "edu.wisc.ssec.mcidasv.data.adde.AddePointDataSource");
506        encoder.registerNewClassName(
507            "edu.wisc.ssec.mcidasv.data.AddeSoundingAdapter",
508            "edu.wisc.ssec.mcidasv.data.adde.AddeSoundingAdapter");
509    }
510    
511    /**
512     * Returns <i>all</i> of the actions used in this McIDAS-V session. This is
513     * possibly TMI and might be removed...
514     * 
515     * @return Actions executed thus far.
516     */
517    public List<String> getActionHistory() {
518        return actions;
519    }
520    
521    /**
522     * Converts {@link ArgsManager#getOriginalArgs()} to a {@link List} and
523     * returns.
524     * 
525     * @return The command-line arguments used to start McIDAS-V, as an 
526     * {@code ArrayList}.
527     */
528    public List<String> getCommandLineArgs() {
529        String[] originalArgs = getArgsManager().getOriginalArgs();
530        List<String> args = arrList(originalArgs.length);
531        Collections.addAll(args, originalArgs);
532        return args;
533    }
534    
535    /**
536     * Captures the action passed to {@code handleAction}. The action is logged
537     * and additionally, if the action is a HTML link, we attempt to visit the
538     * link in the user's preferred browser.
539     */
540    @Override public boolean handleAction(String action, Hashtable properties,
541                                          boolean checkForAlias)
542    {
543        actions.add(action);
544        
545        boolean result = false;
546        DateTime start = DateTime.now();
547        logger.trace("started: action='{}', checkForAlias={}, properties='{}'", action, checkForAlias, properties);
548        if (IOUtil.isHtmlFile(action)) {
549            WebBrowser.browse(action);
550            result = true;
551        } else {
552            if (action.toLowerCase().contains("showsupportform")) {
553                logger.trace("showing support form 'manually'...");
554                getIdvUIManager().showSupportForm();
555                result = true;
556            } else {
557                result = super.handleAction(action, properties, checkForAlias);
558            }
559        }
560        long duration = new DateTime().minus(start.getMillis()).getMillis();
561        logger.trace("finished: action='{}', duration: {} (ms), checkForAlias={}, properties='{}'", action, duration, checkForAlias, properties);
562        
563        return result;
564    }
565    
566    /**
567     * This method checks if the given action is one of the following.
568     * <ul>
569     *   <li>Jython code: starts with {@literal "jython:"}.</li>
570     *   <li>Help link: starts with {@literal "help:"}.</li>
571     *   <li>Resource bundle file: ends with {@literal ".rbi"}.</li>
572     *   <li>Bundle file: ends with {@literal ".xidv"}.</li>
573     *   <li>JNLP file: ends with {@literal ".jnlp"}.</li>
574     * </ul>
575     *
576     * <p>It returns {@code true} if the action is one of these. {@code false}
577     * otherwise.</p>
578     *
579     * @param action The string action
580     * @param properties any properties
581     *
582     * @return {@code true} if the action was {@literal "handled"};
583     * {@code false} otherwise.
584     */
585    @Override protected boolean handleFileOrUrlAction(String action,
586                                                      Hashtable properties) {
587        boolean result = false;
588        boolean idvAction = action.startsWith("idv:");
589        boolean jythonAction = action.startsWith("jython:");
590        
591        if (!idvAction && !jythonAction) {
592            return super.handleFileOrUrlAction(action, properties);
593        }
594        
595        Map<String, Object> hashProps;
596        if (properties != null) {
597            hashProps = new HashMap<>(properties);
598        } else {
599            //noinspection CollectionWithoutInitialCapacity
600            hashProps = new HashMap<>();
601        }
602        
603        ucar.unidata.idv.JythonManager jyManager = getJythonManager();
604        if (idvAction) {
605            action = action.replace("&", "&amp;").substring(4);
606            jyManager.evaluateUntrusted(action, hashProps);
607            result = true;
608        } else if (jythonAction) {
609            action = action.substring(7);
610            jyManager.evaluateAction(action, hashProps);
611            result = true;
612        } else {
613            result = super.handleFileOrUrlAction(action, properties);
614        }
615        return result;
616    }
617    
618    /**
619     * Add a new {@link ControlDescriptor} into the {@code controlDescriptor}
620     * list and {@code controlDescriptorMap}.
621     *
622     * <p>This method differs from the IDV's in that McIDAS-V <b>overwrites</b>
623     * existing {@code ControlDescriptor ControlDescriptors} if
624     * {@link ControlDescriptor#getControlId()} matches.
625     *
626     * @param cd The ControlDescriptor to add.
627     *
628     * @throws NullPointerException if {@code cd} is {@code null}.
629     */
630    @Override protected void addControlDescriptor(ControlDescriptor cd) {
631        cd = Objects.requireNonNull(cd, "Cannot add a null control descriptor to the list of control descriptors.");
632        String id = cd.getControlId();
633        if (controlDescriptorMap.get(id) == null) {
634            controlDescriptors.add(cd);
635            controlDescriptorMap.put(id, cd);
636        } else {
637            for (int i = 0; i < controlDescriptors.size(); i++) {
638                ControlDescriptor tmp = (ControlDescriptor)controlDescriptors.get(i);
639                if (tmp.getControlId().equals(id)) {
640                    controlDescriptors.set(i, cd);
641                    controlDescriptorMap.put(id, cd);
642                    break;
643                }
644            }
645        }
646    }
647    
648    /**
649     * Get the {@code Hashtable} that maps {@link ControlDescriptor} IDs to the
650     * {@code ControlDescriptor} instance.
651     * 
652     * <p>Mostly used as a convenient way to check if a given 
653     * {@code ControlDescriptor} has already been processed.</p>
654     * 
655     * @return {@code Hashtable} that maps {@code String} IDs to the 
656     *         corresponding {@code ControlDescriptor}. 
657     */
658    public Hashtable<String, ControlDescriptor> getControlDescriptorMap() {
659        return controlDescriptorMap;
660    }
661    
662    // pop up an incredibly rudimentary window that controls layer viz animation.
663    public void showLayerVisibilityAnimator() {
664        logger.trace("probably should try to do something here.");
665        SwingUtilities.invokeLater(() -> {
666            try {
667                LayerAnimationWindow window = new LayerAnimationWindow();
668                window.setVisible(true);
669            } catch (Exception e) {
670                logger.error("oh no! something happened!", e);
671            }
672        });
673    }
674    
675    /**
676     * Handles removing all loaded data sources.
677     * 
678     * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method
679     * will ignore the user's preferences and remove all data sources.
680     * 
681     * @param showWarning Whether or not to display a warning message before
682     * removing <i>all</i> data sources. See the return details for more.
683     * 
684     * @return Either {@code true} if the user wants to continue showing the
685     * warning dialog, or {@code false} if they've elected to stop showing the
686     * warning. If {@code showWarning} is {@code false}, this method will 
687     * always return {@code false}, as the user isn't interested in seeing the
688     * warning.
689     */
690    public boolean removeAllData(final boolean showWarning) {
691        boolean reallyRemove = false;
692        boolean continueWarning = true;
693        
694        if (getArgsManager().getIsOffScreen()) {
695            super.removeAllDataSources();
696            return continueWarning;
697        }
698        
699        if (showWarning) {
700            Set<WarningResult> result = showWarningDialog(
701                "Confirm Data Removal",
702                "This action will remove all of the data currently loaded in McIDAS-V.<br>Is this what you want to do?",
703                Constants.PREF_CONFIRM_REMOVE_DATA,
704                "Always ask?",
705                "Remove all data",
706                "Do not remove any data");
707            reallyRemove = result.contains(WarningResult.OK);
708            continueWarning = result.contains(WarningResult.SHOW);
709        } else {
710            // user doesn't want to see warning messages.
711            reallyRemove = true;
712            continueWarning = false;
713        }
714        
715        if (reallyRemove) {
716            super.removeAllDataSources();
717        }
718        
719        return continueWarning;
720    }
721    
722    /**
723     * Handles removing all loaded layers ({@literal "displays"} in IDV-land).
724     * 
725     * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method
726     * will ignore the user's preferences and remove all layers.
727     * 
728     * @param showWarning Whether or not to display a warning message before
729     * removing <i>all</i> layers. See the return details for more.
730     * 
731     * @return Either {@code true} if the user wants to continue showing the
732     * warning dialog, or {@code false} if they've elected to stop showing the
733     * warning. If {@code showWarning} is {@code false}, this method will 
734     * always return {@code false}, as the user isn't interested in seeing the
735     * warning.
736     */
737    public boolean removeAllLayers(final boolean showWarning) {
738        boolean reallyRemove = false;
739        boolean continueWarning = true;
740        
741        if (getArgsManager().getIsOffScreen()) {
742            super.removeAllDisplays();
743            ((ViewManagerManager)getVMManager()).disableAllLayerVizAnimations();
744            return continueWarning;
745        }
746        
747        if (showWarning) {
748            Set<WarningResult> result = showWarningDialog(
749                "Confirm Layer Removal",
750                "This action will remove every layer currently loaded in McIDAS-V.<br>Is this what you want to do?",
751                Constants.PREF_CONFIRM_REMOVE_LAYERS,
752                "Always ask?",
753                "Remove all layers",
754                "Do not remove any layers");
755            reallyRemove = result.contains(WarningResult.OK);
756            continueWarning = result.contains(WarningResult.SHOW);
757        } else {
758            // user doesn't want to see warning messages.
759            reallyRemove = true;
760            continueWarning = false;
761        }
762        
763        if (reallyRemove) {
764            super.removeAllDisplays();
765            ((ViewManagerManager)getVMManager()).disableAllLayerVizAnimations();
766        }
767        
768        return continueWarning;
769    }
770    
771    /**
772     * Overridden so that McIDAS-V can prompt the user before removing, if 
773     * necessary.
774     */
775    @Override public void removeAllDataSources() {
776        IdvObjectStore store = getStore();
777        boolean showWarning =
778            store.get(Constants.PREF_CONFIRM_REMOVE_DATA, true);
779        showWarning = removeAllData(showWarning);
780        store.put(Constants.PREF_CONFIRM_REMOVE_DATA, showWarning);
781    }
782    
783    /**
784     * Overridden so that McIDAS-V can prompt the user before removing, if 
785     * necessary.
786     */
787    @Override public void removeAllDisplays() {
788        IdvObjectStore store = getStore();
789        boolean showWarning =
790            store.get(Constants.PREF_CONFIRM_REMOVE_LAYERS, true);
791        showWarning = removeAllLayers(showWarning);
792        store.put(Constants.PREF_CONFIRM_REMOVE_LAYERS, showWarning);
793    }
794    
795    /**
796     * Handles removing all loaded layers ({@literal "displays"} in IDV-land)
797     * and data sources. 
798     * 
799     * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method
800     * will ignore the user's preferences and remove all layers and data.
801     * 
802     * @see #removeAllData(boolean)
803     * @see #removeAllLayers(boolean)
804     */
805    public void removeAllLayersAndData() {
806        boolean reallyRemove = false;
807        boolean continueWarning = true;
808        
809        if (getArgsManager().getIsOffScreen()) {
810            removeAllData(false);
811            removeAllLayers(false);
812        }
813        
814        IdvObjectStore store = getStore();
815        boolean showWarning = store.get(Constants.PREF_CONFIRM_REMOVE_BOTH, true);
816        if (showWarning) {
817            Set<WarningResult> result = showWarningDialog(
818                "Confirm Removal",
819                "This action will remove all of your currently loaded layers and data.<br>Is this what you want to do?",
820                Constants.PREF_CONFIRM_REMOVE_BOTH,
821                "Always ask?",
822                "Remove all layers and data",
823                "Do not remove anything");
824            reallyRemove = result.contains(WarningResult.OK);
825            continueWarning = result.contains(WarningResult.SHOW);
826        } else {
827            // user doesn't want to see warning messages.
828            reallyRemove = true;
829            continueWarning = false;
830        }
831        
832        // don't show the individual warning messages as the user has attempted
833        // to remove *both*
834        if (reallyRemove) {
835            removeAllData(false);
836            removeAllLayers(false);
837        }
838        
839        store.put(Constants.PREF_CONFIRM_REMOVE_BOTH, continueWarning);
840    }
841    
842    /**
843     * Helper method for showing the removal warning dialog. Note that none of
844     * these parameters should be {@code null} or empty.
845     * 
846     * @param title Title of the warning dialog.
847     * @param message Contents of the warning. May contain HTML, but you do 
848     * not need to provide opening and closing {@literal "html"} tags.
849     * @param prefId ID of the preference that controls whether or not the 
850     * dialog should be displayed.
851     * @param prefLabel Brief description of the preference.
852     * @param okLabel Text of button that signals removal.
853     * @param cancelLabel Text of button that signals cancelling removal.
854     * 
855     * @return A {@code Set} of {@link WarningResult WarningResults} that
856     * describes what the user opted to do. Should always contain only
857     * <b>two</b> elements. One for whether or not {@literal "ok"} or
858     * {@literal "cancel"} was clicked, and one for whether or not the warning
859     * should continue to be displayed.
860     */
861    private Set<WarningResult> showWarningDialog(final String title, 
862        final String message, final String prefId, final String prefLabel, 
863        final String okLabel, final String cancelLabel) 
864    {
865        JCheckBox box = new JCheckBox(prefLabel, true);
866        JComponent comp = GuiUtils.vbox(
867            new JLabel("<html>"+message+"</html>"), 
868            GuiUtils.inset(box, new Insets(4, 15, 0, 10)));
869            
870        Object[] options = { okLabel, cancelLabel };
871        int result = JOptionPane.showOptionDialog(
872            LogUtil.getCurrentWindow(),  // parent
873            comp,                        // msg
874            title,                       // title
875            JOptionPane.YES_NO_OPTION,   // option type
876            JOptionPane.WARNING_MESSAGE, // message type
877            (Icon)null,                  // icon?
878            options,                     // selection values
879            options[1]);                 // initial?
880            
881        WarningResult button = WarningResult.CANCEL;
882        if (result == JOptionPane.YES_OPTION) {
883            button = WarningResult.OK;
884        }
885        
886        WarningResult show = WarningResult.HIDE;
887        if (box.isSelected()) {
888            show = WarningResult.SHOW;
889        }
890        
891        return EnumSet.of(button, show);
892    }
893    
894    public void removeTabData() {
895    }
896    
897    public void removeTabLayers() {
898        
899    }
900    
901    public void removeTabLayersAndData() {
902    }
903    
904    /**
905     * Overridden so that McIDAS-V doesn't have to create an entire new
906     * {@link ucar.unidata.idv.ui.IdvWindow} if
907     * {@link VMManager#findViewManager(ViewDescriptor)} can't find an
908     * appropriate ViewManager for {@code viewDescriptor}.
909     * 
910     * <p>Not doing the above causes McIDAS-V to get stuck in a window creation
911     * loop.</p>
912     */
913    @Override public ViewManager getViewManager(ViewDescriptor viewDescriptor,
914        boolean newWindow, String properties) 
915    {
916        ViewManager vm = 
917            getVMManager().findOrCreateViewManager(viewDescriptor, properties);
918        if (vm == null) {
919            vm = super.getViewManager(viewDescriptor, newWindow, properties);
920        }
921        return vm;
922    }
923    
924    /**
925     * Returns a reference to the current McIDAS-V object. Useful for working 
926     * inside static methods. <b>Always check for null when using this 
927     * method</b>.
928     * 
929     * @return Either the current McIDAS-V "god object" or {@code null}.
930     */
931    public static McIDASV getStaticMcv() {
932        return staticMcv;
933    }
934    
935    /**
936     * @see ucar.unidata.idv.IdvBase#setIdv(ucar.unidata.idv.IntegratedDataViewer)
937     */
938    @Override public void setIdv(IntegratedDataViewer idv) {
939        this.idv = idv;
940    }
941    
942    /**
943     * Load the McV properties. All other property files are disregarded.
944     * 
945     * @see ucar.unidata.idv.IntegratedDataViewer#initPropertyFiles(java.util.List)
946     */
947    @Override public void initPropertyFiles(List files) {
948        files.clear();
949        files.add(Constants.PROPERTIES_FILE);
950    }
951    
952    /**
953     * Makes {@link PersistenceManager} save off a default {@literal "layout"}
954     * bundle.
955     */
956    public void doSaveAsDefaultLayout() {
957        Misc.run(() -> ((PersistenceManager)getPersistenceManager()).doSaveAsDefaultLayout());
958    }
959    
960    /**
961     * Determines whether or not a default layout exists.
962     * 
963     * @return {@code true} if there is a default layout, {@code false} 
964     * otherwise.
965     */
966    public boolean hasDefaultLayout() {
967        String path = 
968            getResourceManager().getResources(IdvResourceManager.RSC_BUNDLES)
969                .getWritable();
970        return new File(path).exists();
971    }
972    
973    /**
974     * Called from the menu command to clear the default bundle. Overridden
975     * in McIDAS-V so that we reference the <i>layout</i> rather than the
976     * bundle.
977     */
978    @Override public void doClearDefaults() {
979        if (GuiUtils.showYesNoDialog(null, 
980                "Are you sure you want to delete your default layout?",
981                "Delete confirmation")) {
982            resourceManager.clearDefaultBundles();
983        }
984    }
985    
986    /**
987     * Returns the time it took for McIDAS-V to start up.
988     *
989     * @return These results are created from subtracting the results of two
990     * {@link System#nanoTime()} calls against one another.
991     */
992    public long getStartupDuration() {
993        return estimate;
994    }
995    
996    /**
997     * <p>
998     * Overridden so that the support form becomes non-modal if launched from
999     * an exception dialog.
1000     * </p>
1001     * 
1002     * @see IntegratedDataViewer#addErrorButtons(JDialog, List, String, Throwable)
1003     */
1004    @Override public void addErrorButtons(final JDialog dialog, 
1005        List buttonList, final String msg, final Throwable exc) 
1006    {
1007        JButton supportBtn = new JButton("Support Form");
1008        supportBtn.addActionListener(ae ->
1009            getIdvUIManager().showSupportForm(msg,
1010                                              LogUtil.getStackTrace(exc),
1011                                              null));
1012        buttonList.add(supportBtn);
1013    }
1014    
1015    /**
1016     * This method is useful for storing commandline {@literal "properties"}
1017     * with the user's preferences.
1018     */
1019    private void overridePreferences() {
1020        StateManager stateManager = (StateManager)getStateManager();
1021        int renderThreads = getMaxRenderThreadCount();
1022        stateManager.putPreference(PREF_THREADS_RENDER, renderThreads);
1023        stateManager.putPreference(PREF_THREADS_DATA, getMaxDataThreadCount());
1024        visad.util.ThreadManager.setGlobalMaxThreads(renderThreads);
1025    }
1026    
1027    /**
1028     * Determine if the last {@literal "exit"} was clean--whether or not
1029     * {@code SESSION_FILE} was removed before the McIDAS-V process terminated.
1030     *
1031     * <p>If the exit was not clean, the user is prompted to submit a support
1032     * request.</p>
1033     */
1034    private void detectAndHandleCrash() {
1035        GuiUtils.setApplicationTitle("");
1036        if (cleanExit || getArgsManager().getIsOffScreen()) {
1037            return;
1038        }
1039        
1040        String msg = "The previous McIDAS-V session did not exit cleanly.<br>"+
1041            "Do you want to send the log file to the McIDAS Help Desk?";
1042        if (previousStart != null) {
1043            msg = "The previous McIDAS-V session (start time: %s) did not exit cleanly.<br>"+
1044                "Do you want to send the log file to the McIDAS Help Desk?";
1045            msg = String.format(msg, previousStart);
1046        }
1047        
1048        boolean continueAsking = getStore().get("mcv.crash.boom.send.report", true);
1049        if (!continueAsking) {
1050            return;
1051        }
1052        
1053        Set<WarningResult> result = showWarningDialog(
1054            "Report Crash",
1055            msg,
1056            "mcv.crash.boom.send.report",
1057            "Always ask?",
1058            "Open support form",
1059            "Do not report");
1060            
1061        getStore().put("mcv.crash.boom.send.report", result.contains(WarningResult.SHOW));
1062        if (!result.contains(WarningResult.OK)) {
1063            return;
1064        }
1065        
1066        getIdvUIManager().showSupportForm();
1067    }
1068    
1069    /**
1070     * Called after the IDV has finished setting everything up after starting.
1071     * McIDAS-V is currently only using this method to determine if the last
1072     * {@literal "exit"} was clean--whether or not {@code SESSION_FILE} was 
1073     * removed before the McIDAS-V process terminated.
1074     *
1075     * Called after the IDV has finished setting everything up. McIDAS-V uses
1076     * this method to handle:
1077     *
1078     * <ul>
1079     *   <li>Clearing out the automatic display creation arguments.</li>
1080     *   <li>Presence of certain properties on the commandline.</li>
1081     *   <li>Detection and handling of a crashed McIDAS-V session.</li>
1082     *   <li>Run action specified by {@code -doaction} flag (if any).</li>
1083     *   <li>Allowing tooltips to remain visible for more than 4 seconds.</li>
1084     * </ul>
1085     *
1086     * @see ArgumentManager#clearAutomaticDisplayArgs()
1087     * @see #overridePreferences()
1088     * @see #detectAndHandleCrash()
1089     */
1090    @Override public void initDone() {
1091        ((ArgumentManager)argsManager).clearAutomaticDisplayArgs();
1092        
1093        overridePreferences();
1094        
1095        detectAndHandleCrash();
1096        
1097        if (addeEntries == null) {
1098            getServerManager();
1099        }
1100        addeEntries.startLocalServer();
1101        
1102        estimate = System.nanoTime() - startTime;
1103        logger.info("estimated startup duration: {} ms", estimate / 1.0e6);
1104        System.setProperty("mcv.start.duration", Long.toString(estimate));
1105        
1106        // handle the -doAction <action id> startup option.
1107        ((ArgumentManager)getArgsManager()).runStartupAction();
1108        
1109        // disable idiotic tooltip dismissal (seriously, 4 seconds!?)
1110        ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE);
1111        
1112        // turn on directory monitoring in the file choosers.
1113        startWatchService();
1114        EventBus.publish(Constants.EVENT_FILECHOOSER_START, "init finished");
1115        
1116        EventDispatchThreadHangMonitor.initMonitoring();
1117    }
1118    
1119    /**
1120     * @see IntegratedDataViewer#doOpen(String, boolean, boolean)
1121     */
1122    @Override public void doOpen(final String filename,
1123        final boolean checkUserPreference, final boolean andRemove) 
1124    {
1125        doOpenInThread(filename, checkUserPreference, andRemove);
1126    }
1127    
1128    /**
1129     * Have the user select a bundle. If andRemove is true then we remove all
1130     * data sources and displays.
1131     *
1132     * Then we open the bundle and start doing unpersistence things.
1133     *
1134     * @param filename The filename to open
1135     * @param checkUserPreference Should we show, if needed, the
1136     * {@literal "open"} dialog
1137     * @param andRemove If true then first remove all data sources and displays
1138     */
1139    private void doOpenInThread(String filename, boolean checkUserPreference,
1140        boolean andRemove) 
1141    {
1142        boolean overwriteData = false;
1143        if (filename == null) {
1144            if (overwriteDataCbx.getToolTipText() == null) {
1145                overwriteDataCbx.setToolTipText("Change the file paths that the data sources use");
1146            }
1147            
1148            filename = FileManager.getReadFile("Open File",
1149                ((ArgumentManager)getArgsManager()).getBundleFilters(true), 
1150                GuiUtils.top(overwriteDataCbx));
1151                
1152            if (filename == null) {
1153                return;
1154            }
1155            
1156            overwriteData = overwriteDataCbx.isSelected();
1157        }
1158        
1159        if (ArgumentManager.isXmlBundle(filename)) {
1160            getPersistenceManager().decodeXmlFile(filename,
1161                checkUserPreference, overwriteData);
1162            return;
1163        }
1164        handleAction(filename, null);
1165    }
1166    
1167    /**
1168     * Factory method to create the McIDAS-V @link JythonManager}.
1169     *
1170     * @return New {@code JythonManager}.
1171     */
1172    @Override protected JythonManager doMakeJythonManager() {
1173        logger.debug("returning a new JythonManager");
1174        return new JythonManager(this);
1175    }
1176    
1177    /**
1178     * Factory method to create a McIDAS-V {@link CollaborationManager}.
1179     *
1180     * @return New {@code CollaborationManager}.
1181     */
1182    @Override protected CollabManager doMakeCollabManager() {
1183        return new CollaborationManager(this);
1184    }
1185    
1186    /**
1187     * Factory method to create a McIDAS-V {@link McIdasChooserManager}.
1188     * Here we create our own manager so it can do things specific to McIDAS-V.
1189     *
1190     * @return {@code McIdasChooserManager} indicated by the startup properties.
1191     * 
1192     * @see ucar.unidata.idv.IdvBase#doMakeIdvChooserManager()
1193     */
1194    @Override
1195    protected IdvChooserManager doMakeIdvChooserManager() {
1196        chooserManager = (McIdasChooserManager)makeManager(
1197            McIdasChooserManager.class, new Object[] { this });
1198        chooserManager.init();
1199        return chooserManager;
1200    }
1201    
1202    /**
1203     * Factory method to create the {@link IdvUIManager}. Here we create our
1204     * own UI manager so it can do things specific to McIDAS-V.
1205     *
1206     * @return {@link UIManager} indicated by the startup properties.
1207     * 
1208     * @see ucar.unidata.idv.IdvBase#doMakeIdvUIManager()
1209     */
1210    @Override
1211    protected IdvUIManager doMakeIdvUIManager() {
1212        return new UIManager(this);
1213    }
1214    
1215    /**
1216     * Create our own VMManager so that we can make the tabs play nice.
1217     * @see ucar.unidata.idv.IdvBase#doMakeVMManager()
1218     */
1219    @Override
1220    protected VMManager doMakeVMManager() {
1221        // what an ugly class name :(
1222        return new ViewManagerManager(this);
1223    }
1224    
1225    /**
1226     * Make the {@link McIdasPreferenceManager}.
1227     * @see ucar.unidata.idv.IdvBase#doMakePreferenceManager()
1228     */
1229    @Override
1230    protected IdvPreferenceManager doMakePreferenceManager() {
1231        return new McIdasPreferenceManager(this);
1232    }
1233    
1234    /**
1235     * <p>McIDAS-V (alpha 10+) needs to handle both IDV bundles without 
1236     * component groups and all bundles from prior McV alphas. You better 
1237     * believe we need to extend the persistence manager functionality!</p>
1238     * 
1239     * @see ucar.unidata.idv.IdvBase#doMakePersistenceManager()
1240     */
1241    @Override protected IdvPersistenceManager doMakePersistenceManager() {
1242        return new PersistenceManager(this);
1243    }
1244    
1245    /**
1246     * Create, if needed, and return the {@link McIdasChooserManager}.
1247     * 
1248     * @return The Chooser manager
1249     */
1250    public McIdasChooserManager getMcIdasChooserManager() {
1251        return (McIdasChooserManager)getIdvChooserManager();
1252    }
1253    
1254    /**
1255     * Returns the {@link MonitorManager}.
1256     *
1257     * @return McIDAS-V {@literal "monitor manager"}.
1258     */
1259    public MonitorManager getMonitorManager() {
1260        return monitorManager;
1261    }
1262    
1263    /**
1264     * Responds to events generated by the server manager's GUI. Currently
1265     * limited to {@link edu.wisc.ssec.mcidasv.servermanager.TabbedAddeManager.Event#CLOSED TabbedAddeManager.Event#CLOSED}.
1266     *
1267     * @param evt {@code TabbedAddeManager} event to respond to.
1268     */
1269    @EventSubscriber(eventClass=TabbedAddeManager.Event.class)
1270    public void onServerManagerWindowEvent(TabbedAddeManager.Event evt) {
1271        if (evt == TabbedAddeManager.Event.CLOSED) {
1272            tabbedAddeManager = null;
1273        }
1274    }
1275    
1276    /**
1277     * Creates (if needed) the server manager GUI and displays it.
1278     */
1279    public void showServerManager() {
1280        if (tabbedAddeManager == null) {
1281            tabbedAddeManager = new TabbedAddeManager(getServerManager());
1282        }
1283        tabbedAddeManager.showManager();
1284    }
1285    
1286    /**
1287     * Creates a new server manager (if needed) and returns it.
1288     *
1289     * @return The McIDAS-V ADDE server manager.
1290     */
1291    public EntryStore getServerManager() {
1292        if (addeEntries == null) {
1293            addeEntries = new EntryStore(getStore(), getResourceManager());
1294        }
1295        return addeEntries;
1296    }
1297    
1298    public McvDataManager getMcvDataManager() {
1299        return (McvDataManager)getDataManager();
1300    }
1301    
1302    /**
1303     * Get McIDASV. 
1304     * @see ucar.unidata.idv.IdvBase#getIdv()
1305     */
1306    @Override public IntegratedDataViewer getIdv() {
1307        return this;
1308    }
1309    
1310    /**
1311     * Creates a McIDAS-V argument manager so that McV can handle some non-IDV
1312     * command line things.
1313     * 
1314     * @param args The arguments from the command line.
1315     * 
1316     * @see ucar.unidata.idv.IdvBase#doMakeArgsManager(java.lang.String[])
1317     */
1318    @Override protected ArgsManager doMakeArgsManager(String[] args) {
1319        return new ArgumentManager(this, args);
1320    }
1321    
1322    /**
1323     * Factory method to create the {@link McvDataManager}.
1324     * 
1325     * @return The data manager
1326     * 
1327     * @see ucar.unidata.idv.IdvBase#doMakeDataManager()
1328     */
1329    @Override protected DataManager doMakeDataManager() {
1330        return new McvDataManager(this);
1331    }
1332    
1333    /**
1334     * Make the McIDAS-V {@link StateManager}.
1335     * @see ucar.unidata.idv.IdvBase#doMakeStateManager()
1336     */
1337    @Override protected StateManager doMakeStateManager() {
1338        return new StateManager(this);
1339    }
1340    
1341    /**
1342     * Make the McIDAS-V {@link ResourceManager}.
1343     * @see ucar.unidata.idv.IdvBase#doMakeResourceManager()
1344     */
1345    @Override protected IdvResourceManager doMakeResourceManager() {
1346        return new ResourceManager(this);
1347    }
1348    
1349    /**
1350     * Make the {@link McIdasColorTableManager}.
1351     * @see ucar.unidata.idv.IdvBase#doMakeColorTableManager()
1352     */
1353    @Override protected ColorTableManager doMakeColorTableManager() {
1354        return new McIdasColorTableManager();
1355    }
1356    
1357    /**
1358     * Factory method to create the {@link McvPluginManager}.
1359     *
1360     * @return The McV plugin manager.
1361     * 
1362     * @see ucar.unidata.idv.IdvBase#doMakePluginManager()
1363     */
1364    @Override protected PluginManager doMakePluginManager() {
1365        return new McvPluginManager(this);
1366    }
1367    
1368//    /**
1369//     * Make the {@link edu.wisc.ssec.mcidasv.data.McIDASVProjectionManager}.
1370//     * @see ucar.unidata.idv.IdvBase#doMakeIdvProjectionManager()
1371//     */
1372//    @Override
1373//    protected IdvProjectionManager doMakeIdvProjectionManager() {
1374//      return new McIDASVProjectionManager(this);
1375//    }
1376    
1377    /**
1378     * Make a help button for a particular help topic
1379     *
1380     * @param helpId  the topic id
1381     * @param toolTip  the tooltip
1382     *
1383     * @return  the button
1384     */
1385    @Override public JComponent makeHelpButton(String helpId, String toolTip) {
1386        JButton btn = McVGuiUtils.makeImageButton(Constants.ICON_HELP,
1387            getIdvUIManager(), "showHelp", helpId, "Show help");
1388            
1389        if (toolTip != null) {
1390            btn.setToolTipText(toolTip);
1391        }
1392        return btn;
1393    }
1394    
1395    /**
1396     * Return the current {@literal "userpath"}.
1397     * 
1398     * @return Path to the user's {@literal "McIDAS-V directory"}.
1399     */
1400    public String getUserDirectory() {
1401        return StartupManager.getInstance().getPlatform().getUserDirectory();
1402    }
1403    
1404    /**
1405     * Return the path to a file within {@literal "userpath"}.
1406     * 
1407     * @param filename File within the userpath.
1408     * 
1409     * @return Path to a file within the user's {@literal "McIDAS-V directory"}.
1410     * No path validation is performed, so please be aware that the returned
1411     * path may not exist.
1412     */
1413    public String getUserFile(String filename) {
1414        return StartupManager.getInstance().getPlatform().getUserFile(filename);
1415    }
1416    
1417    /**
1418     * Invokes the main method for a given class. 
1419     * 
1420     * <p>Note: this is rather limited so far as it doesn't pass in any
1421     * arguments.</p>
1422     * 
1423     * @param className Class whose main method is to be invoked. Cannot be
1424     * {@code null}.
1425     */
1426    public void runPluginMainMethod(final String className) {
1427        final String[] dummyArgs = { "" };
1428        try {
1429            Class<?> clazz = Misc.findClass(className);
1430            Class[] args = new Class[] { dummyArgs.getClass() };
1431            Method mainMethod = Misc.findMethod(clazz, "main", args);
1432            if (mainMethod != null) {
1433                mainMethod.invoke(null, new Object[] { dummyArgs });
1434            }
1435        } catch (Exception e) {
1436            logger.error("problem with plugin class", e);
1437            LogUtil.logException("problem running main method for class: "+className, e);
1438        }
1439    }
1440    
1441    /**
1442     * Attempts to determine if a given string is a 
1443     * {@literal "loopback address"} (aka localhost).
1444     * 
1445     * <p>Strings are <b>trimmed and converted to lowercase</b>, and currently
1446     * checked against:
1447     * <ul>
1448     * <li>{@code 127.0.0.1}</li>
1449     * <li>{@code ::1} (for IPv6)</li>
1450     * <li>Strings starting with {@code localhost}.</li>
1451     * </ul>
1452     * 
1453     * @param host {@code String} to check. Should not be {@code null}.
1454     * 
1455     * @return {@code true} if {@code host} is a recognized loopback address.
1456     * {@code false} otherwise.
1457     * 
1458     * @throws NullPointerException if {@code host} is {@code null}.
1459     */
1460    public static boolean isLoopback(final String host) {
1461        String cleaned = Objects.requireNonNull(host.trim().toLowerCase());
1462        return "127.0.0.1".startsWith(cleaned) 
1463            || "::1".startsWith(cleaned) 
1464            || cleaned.startsWith("localhost");
1465    }
1466    
1467    /**
1468     * Are we on a Mac? Used to build the MRJ handlers, taken from TN2110.
1469     * 
1470     * @return {@code true} if this session is running on top of OS X,
1471     * {@code false} otherwise.
1472     * 
1473     * @see <a href="http://developer.apple.com/technotes/tn2002/tn2110.html">TN2110</a>
1474     */
1475    public static boolean isMac() {
1476        String osName = System.getProperty("os.name");
1477        return osName.contains("OS X");
1478    }
1479    
1480    /**
1481     * Queries the {@code os.name} system property and if the result does not 
1482     * start with {@literal "Windows"}, the platform is assumed to be 
1483     * {@literal "unix-like"}.
1484     * 
1485     * <p>Given the McIDAS-V supported platforms (Windows, {@literal "Unix"}, 
1486     * and OS X), the above logic is safe.
1487     * 
1488     * @return {@code true} if we're not running on Windows, {@code false} 
1489     * otherwise.
1490     * 
1491     * @throws RuntimeException if there is no property associated with 
1492     * {@code os.name}.
1493     */
1494    public static boolean isUnixLike() {
1495        String osName = System.getProperty("os.name");
1496        if (osName == null) {
1497            throw new RuntimeException("no os.name system property!");
1498        }
1499        
1500        if (System.getProperty("os.name").startsWith("Windows")) {
1501            return false;
1502        }
1503        return true;
1504    }
1505    
1506    /**
1507     * Queries the {@code os.name} system property and if the result starts 
1508     * with {@literal "Windows"}, the platform is assumed to be Windows. Duh.
1509     * 
1510     * @return {@code true} if we're running on Windows, {@code false} 
1511     * otherwise.
1512     * 
1513     * @throws RuntimeException if there is no property associated with 
1514     * {@code os.name}.
1515     */
1516    public static boolean isWindows() {
1517        String osName = System.getProperty("os.name");
1518        if (osName == null) {
1519            throw new RuntimeException("no os.name system property!");
1520        }
1521        
1522        return osName.startsWith("Windows");
1523    }
1524    
1525    /**
1526     * If McIDAS-V is running on Windows, this method will return a 
1527     * {@code String} that looks like {@literal "C:"} or {@literal "D:"}, etc.
1528     * 
1529     * <p>If McIDAS-V is not running on Windows, this method will return an
1530     * empty {@code String}.
1531     * 
1532     * @return Either the {@literal "drive letter"} of the {@code java.home} 
1533     * property or an empty {@code String} if McIDAS-V isn't running on Windows.
1534     * 
1535     * @throws RuntimeException if there is no property associated with 
1536     * {@code java.home}.
1537     */
1538    public static String getJavaDriveLetter() {
1539        if (!isWindows()) {
1540            return "";
1541        }
1542        
1543        String home = System.getProperty("java.home");
1544        if (home == null) {
1545            throw new RuntimeException("no java.home system property!");
1546        }
1547        
1548        return home.substring(0, 2);
1549    }
1550    
1551    /**
1552     * Attempts to create a {@literal "session"} file. This method will create
1553     * a {@literal "userpath"} if it does not already exist. 
1554     * 
1555     * @param path Path of the session file that should get created. 
1556     * {@code null} values are not allowed, and sufficient priviledges are 
1557     * assumed.
1558     * 
1559     * @throws AssertionError if McIDAS-V couldn't write to {@code path}.
1560     * 
1561     * @see #SESSION_FILE
1562     * @see #hadCleanExit(String)
1563     * @see #removeSessionFile(String)
1564     */
1565    private static void createSessionFile(final String path) {
1566        assert path != null : "Cannot create a null path";
1567        try (
1568            FileOutputStream out = new FileOutputStream(path);
1569            PrintStream p = new PrintStream(out)
1570        ) {
1571            p.println(System.currentTimeMillis());
1572        } catch (Exception e) {
1573            throw new AssertionError("Could not write to "+path+". Error message: "+e.getMessage(), e);
1574        }
1575    }
1576    
1577    /**
1578     * Attempts to extract a timestamp from {@code path}. {@code path} is 
1579     * expected to <b>only</b> contain a single line consisting of a 
1580     * {@link Long} integer.
1581     * 
1582     * @param path Path to the file of interest.
1583     * 
1584     * @return Either a {@link Date} of the timestamp contained in 
1585     * {@code path} or {@code null} if the extraction failed.
1586     */
1587    private static Date extractDate(final String path) {
1588        assert path != null;
1589        Date savedDate = null;
1590        try (BufferedReader r = new BufferedReader(new FileReader(path))) {
1591            String line = r.readLine();
1592            if (line != null) {
1593                savedDate = new Date(Long.parseLong(line.trim()));
1594            }
1595        } catch (Exception e) {
1596            logger.warn("Could not extract date from session file", e);
1597        }
1598        return savedDate;
1599    }
1600    
1601    /**
1602     * Attempts to remove the file accessible via {@code path}.
1603     * 
1604     * @param path Path of the file that'll get removed. This should be 
1605     * non-null and point to an existing and writable filename (not a 
1606     * directory).
1607     * 
1608     * @throws AssertionError if the file at {@code path} could not be 
1609     * removed.
1610     * 
1611     * @see #SESSION_FILE
1612     * @see #createSessionFile(String)
1613     * @see #hadCleanExit(String)
1614     */
1615    private static void removeSessionFile(final String path) {
1616        if (path == null) {
1617            return;
1618        }
1619        
1620        File f = new File(path);
1621        
1622        if (!f.exists() || !f.canWrite() || f.isDirectory()) {
1623            return;
1624        }
1625        
1626        if (!f.delete()) {
1627            throw new AssertionError("Could not delete session file");
1628        }
1629    }
1630    
1631    /**
1632     * Tries to determine whether or not the last McIDAS-V session ended 
1633     * {@literal "cleanly"}. Currently a simple check for a 
1634     * {@literal "session"} file that is created upon starting and removed upon
1635     * ending.
1636     * 
1637     * @param path Path to the session file to check. Can't be {@code null}.
1638     * 
1639     * @return Either {@code true} if the file pointed at by {@code path} does
1640     * <b><i>NOT</i></b> exist, {@code false} if it does exist.
1641     * 
1642     * @see #SESSION_FILE
1643     * @see #createSessionFile(String)
1644     * @see #removeSessionFile(String)
1645     */
1646    private static boolean hadCleanExit(final String path) {
1647        assert path != null : "Cannot test for a null path";
1648        return !(new File(path).exists());
1649    }
1650    
1651    /**
1652     * Returns the (<i>current</i>) path to the session file. Note that the
1653     * location of the file may change arbitrarily.
1654     * 
1655     * @return {@code String} pointing to the session file.
1656     * 
1657     * @see #SESSION_FILE
1658     */
1659    public static String getSessionFilePath() {
1660        return StartupManager.getInstance().getPlatform().getUserFile("session.tmp");
1661    }
1662    
1663    /**
1664     * Useful for providing the startup manager with values other than the 
1665     * defaults... Note that this method attempts to update the value of 
1666     * {@link #SESSION_FILE}.
1667     * 
1668     * @param args Likely the argument array coming from the main method.
1669     */
1670    private static void applyArgs(final String[] args) {
1671        assert args != null : "Cannot use a null argument array";
1672        StartupManager.applyArgs(true, false, args);
1673        SESSION_FILE = getSessionFilePath();
1674    }
1675    
1676    /**
1677     * This returns the set of {@link ControlDescriptor ControlDescriptors}
1678     * that can be shown. The ordering of this list determines the
1679     * "default" controls shown in the Field Selector, so we override
1680     * here for control over the ordering.
1681     *
1682     * @param includeTemplates If true then include the display templates
1683     * @return re-ordered List of shown control descriptors
1684     */
1685    @Override public List getControlDescriptors(boolean includeTemplates) {
1686        List<ControlDescriptor> l = 
1687            cast(super.getControlDescriptors(includeTemplates));
1688        for (int i = 0; i < l.size(); i++) {
1689            ControlDescriptor cd = l.get(i);
1690            if (cd.getControlId().equals("omni")) {
1691                // move the omni control to the end of the list
1692                // so it will never be "default" in Field Selector
1693                l.remove(i);
1694                l.add(cd);
1695            }
1696        }
1697        return l;
1698    }
1699    
1700    /**
1701     * Show the McIDAS-V {@literal "Welcome Window"} for the first start up.
1702     *
1703     * @param args Commandline arguments, used to handle autoquit stress testing.
1704     */
1705    private static void handleWelcomeWindow(String... args) {
1706        boolean showWelcome = false;
1707        boolean welcomeAutoQuit = false;
1708        long welcomeAutoQuitDelay = WelcomeWindow.DEFAULT_QUIT_DELAY;
1709        for (int i = 0; i < args.length; i++) {
1710            if ("-welcomewindow".equals(args[i])) {
1711                showWelcome = true;
1712            } else if ("-autoquit".equals(args[i])) {
1713                welcomeAutoQuit = true;
1714                int delayIdx = i + 1;
1715                if (delayIdx < args.length) {
1716                    welcomeAutoQuitDelay = Long.parseLong(args[delayIdx]);
1717                }
1718            }
1719        }
1720        
1721        // if user elects to quit, System.exit(1) will be called.
1722        // if the user decides to start, the welcome window will be simply be
1723        // closed, allowing McV to continue starting up.
1724        if (showWelcome) {
1725            WelcomeWindow ww;
1726            if (welcomeAutoQuit) {
1727                ww = new WelcomeWindow(true, welcomeAutoQuitDelay);
1728            } else {
1729                ww = new WelcomeWindow();
1730            }
1731            ww.setVisible(true);
1732        }
1733    }
1734    
1735    /**
1736     * Register {@literal "adde"} and {@literal "idvresource"} URL protocols.
1737     *
1738     * <p>This needs to be called pretty early in the McIDAS-V initialization
1739     * process. They're currently being registered immediately after the
1740     * session file is created.</p>
1741     */
1742    private static void registerProtocolHandlers() {
1743        try {
1744            URL.setURLStreamHandlerFactory(protocol -> {
1745                switch (protocol.toLowerCase()) {
1746                    case AddeURL.ADDE_PROTOCOL:
1747                        return new AddeURLStreamHandler();
1748                    case PluginManager.PLUGIN_PROTOCOL:
1749                        return new IdvResourceStreamHandler();
1750                    default:
1751                        return null;
1752                }
1753            });
1754        } catch (Throwable e) {
1755            logger.error("Could not register protocol handlers!", e);
1756        }
1757    }
1758    
1759    /**
1760     * Responsible for handling {@literal "idvresource"} URLs.
1761     *
1762     * <p>Really just a redirect to {@link IOUtil#getURL(String, Class)}.</p>
1763     */
1764    private static class IdvResourceStreamHandler extends URLStreamHandler {
1765        @Override protected URLConnection openConnection(URL u)
1766            throws IOException
1767        {
1768            return IOUtil.getURL(u.getPath(), McIDASV.class).openConnection();
1769        }
1770    }
1771
1772    /**
1773     * Check the value of the {@code mcidasv.darkmode} system property.
1774     *
1775     * @return {@code true} if dark mode should be enabled, {@code false} otherwise.
1776     */
1777    public static boolean isDarkMode() {
1778        return Boolean.parseBoolean(System.getProperty("mcidasv.darkmode", "false"));
1779    }
1780
1781    /**
1782     * Configure the logging and create the McIDAS-V object responsible for
1783     * initializing the application session.
1784     *
1785     * @param args Command line arguments.
1786     *
1787     * @throws Exception When something untoward happens.
1788     */
1789    public static void main(String... args) throws Exception {
1790        // show the welcome window if needed.
1791        // since the welcome window is intended to be a one time thing,
1792        // it probably shouldn't count for the startTime stuff.
1793        handleWelcomeWindow(args);
1794        
1795        startTime = System.nanoTime();
1796        
1797        // allow use of the "unlimited strength" crypto policy.
1798        // this becomes the default in 1.8.0_162, but we need to ship with
1799        // 1.8.0_152.
1800        Security.setProperty("crypto.policy", "unlimited");
1801
1802        // Configure GUI look and feel before any UI stuff
1803        if (isDarkMode()) {
1804            FlatDarkLaf.setup();
1805        }
1806
1807        // the following two lines are required if we want to embed JavaFX
1808        // widgets into McV (which is Swing). The first line initializes the
1809        // JavaFX runtime, and the second line allows the JavaFX runtime to
1810        // hang around even if there are no JavaFX windows.
1811         JFXPanel dummy = new JFXPanel();
1812         Platform.setImplicitExit(false);
1813        
1814        try {
1815            applyArgs(args);
1816            
1817            SysOutOverSLF4J.sendSystemOutAndErrToSLF4J(LogLevel.INFO, LogLevel.WARN);
1818            
1819            // Optionally remove existing handlers attached to j.u.l root logger
1820            SLF4JBridgeHandler.removeHandlersForRootLogger();  // (since SLF4J 1.6.5)
1821            
1822            // add SLF4JBridgeHandler to j.u.l's root logger, should be done once during
1823            // the initialization phase of your application
1824            SLF4JBridgeHandler.install();
1825            
1826//            Properties pythonProps = new Properties();
1827//            logger.trace("calling PythonInterpreter.initialize...");
1828//            PythonInterpreter.initialize(System.getProperties(), pythonProps, new String[] {""});
1829            
1830            LogUtil.configure();
1831            
1832            // Register IOSPs here
1833            
1834            // TJJ Jan 2019 - new IOSP for TropOMI data
1835            NetcdfFile.registerIOProvider(TropomiIOSP.class);
1836            // Experimental - this was 2016ish for GPM data, should still work
1837            NetcdfFile.registerIOProvider(GpmIosp.class);
1838            
1839            long sysMem = Long.valueOf(SystemState.queryOpSysProps().get("opsys.memory.physical.total"));
1840            logger.info("=============================================================================");
1841            logger.info("Starting McIDAS-V @ {}", new Date());
1842            logger.info("Versions:");
1843            logger.info("{}", SystemState.getMcvVersionString());
1844            logger.info("{}", SystemState.getIdvVersionString());
1845            logger.info("{}", SystemState.getVisadVersionString());
1846            logger.info("{}", SystemState.getNcidvVersionString());
1847            logger.info("{} MB system memory", Math.round(sysMem/1024/1024));
1848            
1849            if (!hadCleanExit(SESSION_FILE)) {
1850                previousStart = extractDate(SESSION_FILE);
1851            }
1852            
1853            createSessionFile(SESSION_FILE);
1854            registerProtocolHandlers();
1855            
1856            McIDASV myself = new McIDASV(args);
1857        } catch (IllegalArgumentException e) {
1858            String msg = "McIDAS-V could not initialize itself. ";
1859            String osName = System.getProperty("os.name");
1860            if (osName.contains("Windows")) {
1861                LogUtil.userErrorMessage(msg+'\n'+e.getMessage());
1862            } else {
1863                System.err.println(msg+e.getMessage());
1864            }
1865        }
1866    }
1867    
1868    /**
1869     * Log (TRACE level) the time spent in various discrete parts of
1870     * McIDAS-V's startup process.
1871     *
1872     * <p>Currently tracking time spent:
1873     * <ul>
1874     *   <li>creating GUI components with {@link McIDASVXmlUi}</li>
1875     *   <li>initialing Jython interpreters.</li>
1876     * </ul></p>
1877     * 
1878     * <p><b>The elapsed times are merely a quick estimate.</b> The only way
1879     * to obtain accurate timing information with the JVM is using 
1880     * <a href="https://openjdk.java.net/projects/code-tools/jmh/">JMH</a>.</p>
1881     *
1882     * @see McIDASVXmlUi#getElapsedGuiTime()
1883     * @see JythonManager#getElapsedInterpreterInit()
1884     */
1885    public static void elapsedStartup() {
1886        long totalXmlUi = 0L;
1887        List<IdvWindow> windows = cast(IdvWindow.getWindows());
1888        for (IdvWindow window : windows) {
1889            McIDASVXmlUi xmlUi = (McIDASVXmlUi)window.getXmlUI();
1890            if (xmlUi != null) {
1891                long winElapsed = xmlUi.getElapsedGuiTime();
1892                totalXmlUi += winElapsed;
1893                logger.trace("title '{}' xmlui '{}' spent {} ms creating components",
1894                             window.getTitle(),
1895                             Integer.toHexString(xmlUi.hashCode()),
1896                             String.format("%.2f", winElapsed / 1.0e6));
1897            } else {
1898                logger.trace("no xmlui for '{}'", window.getTitle());
1899            }
1900        }
1901        
1902        McIDASV m = getStaticMcv();
1903        long elapsedJython = m.getJythonManager().getElapsedInterpreterInit();
1904        
1905        String xmlui = String.format("%6.2f", totalXmlUi / 1.0e6);
1906        String jython = String.format("%6.2f", elapsedJython / 1.0e6);
1907        logger.trace("estimated time spent creating xmlui GUI components   : {} ms", Strings.padStart(xmlui, 9, ' '));
1908        logger.trace("estimated time spent initializing jython interpreters: {} ms", Strings.padStart(jython, 9, ' '));
1909    }
1910    
1911    /**
1912     * Attempts a clean shutdown of McIDAS-V. Currently this entails 
1913     * suppressing any error dialogs, explicitly killing the 
1914     * {@link #addeEntries}, removing {@link #SESSION_FILE}, and disabling
1915     * the directory monitors found in the file choosers.
1916     * 
1917     * @param exitCode System exit code to use.
1918     * 
1919     * @see IntegratedDataViewer#quit()
1920     */
1921    @Override protected void exit(int exitCode) {
1922        LogUtil.setShowErrorsInGui(false);
1923        
1924        // turn off the directory monitors in the file choosers.
1925        EventBus.publish(Constants.EVENT_FILECHOOSER_STOP, "shutting down");
1926        stopWatchService();
1927        
1928        if (addeEntries != null) {
1929            addeEntries.saveForShutdown();
1930            addeEntries.stopLocalServer();
1931        }
1932        
1933        removeSessionFile(SESSION_FILE);
1934        
1935        // shut down javafx runtime
1936        Platform.exit();
1937        
1938        // dump some information about how much time was spent doing certain
1939        // things during startup. 
1940        elapsedStartup();
1941        
1942        logger.info("Exiting McIDAS-V @ {}", new Date());
1943        
1944        System.exit(exitCode);
1945    }
1946    
1947    /**
1948     * This method is largely a copy of {@link IntegratedDataViewer#quit()},
1949     * but allows for some GUI testing.
1950     */
1951    public boolean autoQuit() {
1952        IdvObjectStore store = getStore();
1953        
1954        boolean showQuitConfirm = store.get(PREF_SHOWQUITCONFIRM, true);
1955        long quitDelay = store.get("mcidasv.autoexit.delay", 3000);
1956        
1957        if (showQuitConfirm) {
1958            JCheckBox cbx = new JCheckBox("Always ask", true);
1959            JComponent comp =
1960                GuiUtils.vbox(
1961                    new JLabel("<html><b>Do you really want to exit?</b></html>"),
1962                    GuiUtils.inset(cbx, new Insets(4, 15, 0, 10)));
1963                    
1964            JOptionPane pane = new JOptionPane(comp,
1965                                               JOptionPane.QUESTION_MESSAGE,
1966                                               JOptionPane.YES_NO_OPTION);
1967                                               
1968            new OptionPaneClicker(pane, "Exit Confirmation", quitDelay, "Yes");
1969            getStore().put(PREF_SHOWQUITCONFIRM, cbx.isSelected());
1970        }
1971        
1972        if (!getStationModelManager().checkCloseWindow()) {
1973            return false;
1974        }
1975        
1976        if (!getJythonManager().saveOnExit()) {
1977            return false;
1978        }
1979        
1980        store.saveIfNeeded();
1981        store.cleanupTmpFiles();
1982        getPluginManager().handleApplicationExit();
1983        getJythonManager().handleApplicationExit();
1984        
1985        if (getInteractiveMode()) {
1986            exit(0);
1987        }
1988        return true;
1989    }
1990    
1991    /**
1992     * Register the given {@code listener} so that changes to files matching
1993     * {@code glob} in {@code path} can be handled.
1994     *
1995     * @param path Directory to watch.
1996     * @param glob Only respond to files matching this file mask.
1997     * @param listener Listener that can handle file changes.
1998     *
1999     * @throws IOException if there was a problem registering {@code listener}.
2000     */
2001    public void watchDirectory(final String path,
2002                               final String glob,
2003                               final OnFileChangeListener listener)
2004        throws IOException
2005    {
2006        watchService.register(listener, path, glob);
2007    }
2008    
2009    /**
2010     * Returns McIDAS-V's {@link DirectoryWatchService}.
2011     *
2012     * @return {@code DirectoryWatchService} responsible for handling all of
2013     * McIDAS-V's directory monitoring.
2014     */
2015    public DirectoryWatchService getWatchService() {
2016        return watchService;
2017    }
2018    
2019    /**
2020     * Enable directory monitoring.
2021     */
2022    public void startWatchService() {
2023        watchService.start();
2024    }
2025    
2026    /**
2027     * Disable directory monitoring.
2028     */
2029    public void stopWatchService() {
2030        if (watchService != null) {
2031            watchService.stop();
2032        }
2033    }
2034    
2035    /**
2036     * Exposes {@link #exit(int)} to other classes.
2037     * 
2038     * @param exitCode System exit code to use.
2039     * 
2040     * @see #exit(int)
2041     */
2042    public void exitMcIDASV(int exitCode) {
2043        exit(exitCode);
2044    }
2045}