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