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