001    /*
002     * $Id: McIDASV.java,v 1.123 2012/02/19 17:35:53 davep Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
007     * Space Science and Engineering Center (SSEC)
008     * University of Wisconsin - Madison
009     * 1225 W. Dayton Street, Madison, WI 53706, USA
010     * https://www.ssec.wisc.edu/mcidas
011     * 
012     * All Rights Reserved
013     * 
014     * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015     * some McIDAS-V source code is based on IDV and VisAD source code.  
016     * 
017     * McIDAS-V is free software; you can redistribute it and/or modify
018     * it under the terms of the GNU Lesser Public License as published by
019     * the Free Software Foundation; either version 3 of the License, or
020     * (at your option) any later version.
021     * 
022     * McIDAS-V is distributed in the hope that it will be useful,
023     * but WITHOUT ANY WARRANTY; without even the implied warranty of
024     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025     * GNU Lesser Public License for more details.
026     * 
027     * You should have received a copy of the GNU Lesser Public License
028     * along with this program.  If not, see http://www.gnu.org/licenses.
029     */
030    
031    package edu.wisc.ssec.mcidasv;
032    
033    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
034    
035    import static ucar.unidata.xml.XmlUtil.getAttribute;
036    
037    import java.awt.Insets;
038    import java.awt.event.ActionEvent;
039    import java.awt.event.ActionListener;
040    import java.awt.geom.Rectangle2D;
041    import java.io.BufferedReader;
042    import java.io.File;
043    import java.io.FileOutputStream;
044    import java.io.FileReader;
045    import java.io.PrintStream;
046    import java.lang.reflect.Method;
047    import java.rmi.RemoteException;
048    import java.util.*;
049    
050    import javax.swing.Icon;
051    import javax.swing.JButton;
052    import javax.swing.JCheckBox;
053    import javax.swing.JComponent;
054    import javax.swing.JDialog;
055    import javax.swing.JLabel;
056    import javax.swing.JOptionPane;
057    import javax.swing.SwingUtilities;
058    
059    import edu.wisc.ssec.mcidasv.util.SystemState;
060    import org.bushe.swing.event.annotation.AnnotationProcessor;
061    import org.bushe.swing.event.annotation.EventSubscriber;
062    import org.slf4j.Logger;
063    import org.slf4j.LoggerFactory;
064    import org.w3c.dom.Element;
065    
066    import visad.VisADException;
067    
068    import ucar.unidata.data.DataManager;
069    import ucar.unidata.idv.ArgsManager;
070    import ucar.unidata.idv.ControlDescriptor;
071    import ucar.unidata.idv.IdvObjectStore;
072    import ucar.unidata.idv.IdvPersistenceManager;
073    import ucar.unidata.idv.IdvPreferenceManager;
074    import ucar.unidata.idv.IdvResourceManager;
075    import ucar.unidata.idv.IntegratedDataViewer;
076    import ucar.unidata.idv.PluginManager;
077    import ucar.unidata.idv.VMManager;
078    import ucar.unidata.idv.ViewDescriptor;
079    import ucar.unidata.idv.ViewManager;
080    import ucar.unidata.idv.chooser.IdvChooserManager;
081    import ucar.unidata.idv.ui.IdvUIManager;
082    import ucar.unidata.ui.colortable.ColorTableManager;
083    import ucar.unidata.util.FileManager;
084    import ucar.unidata.util.GuiUtils;
085    import ucar.unidata.util.IOUtil;
086    import ucar.unidata.util.LogUtil;
087    import ucar.unidata.util.Misc;
088    import ucar.unidata.xml.XmlDelegateImpl;
089    import ucar.unidata.xml.XmlEncoder;
090    import ucar.unidata.xml.XmlUtil;
091    import uk.org.lidalia.sysoutslf4j.context.LogLevel;
092    import uk.org.lidalia.sysoutslf4j.context.SysOutOverSLF4J;
093    
094    import edu.wisc.ssec.mcidasv.chooser.McIdasChooserManager;
095    import edu.wisc.ssec.mcidasv.control.LambertAEA;
096    import edu.wisc.ssec.mcidasv.data.McvDataManager;
097    import edu.wisc.ssec.mcidasv.monitors.MonitorManager;
098    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource;
099    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
100    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
101    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity;
102    import edu.wisc.ssec.mcidasv.servermanager.EntryStore;
103    import edu.wisc.ssec.mcidasv.servermanager.EntryTransforms;
104    import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry;
105    import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry.AddeFormat;
106    import edu.wisc.ssec.mcidasv.servermanager.RemoteAddeEntry;
107    import edu.wisc.ssec.mcidasv.servermanager.TabbedAddeManager;
108    import edu.wisc.ssec.mcidasv.startupmanager.StartupManager;
109    import edu.wisc.ssec.mcidasv.ui.LayerAnimationWindow;
110    import edu.wisc.ssec.mcidasv.ui.McIdasColorTableManager;
111    import edu.wisc.ssec.mcidasv.ui.UIManager;
112    import edu.wisc.ssec.mcidasv.util.Contract;
113    import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
114    import edu.wisc.ssec.mcidasv.util.WebBrowser;
115    
116    @SuppressWarnings("unchecked")
117    public class McIDASV extends IntegratedDataViewer {
118    
119        private static final Logger logger = LoggerFactory.getLogger(McIDASV.class);
120    
121        /** 
122         * Path to a {@literal "session"} file--it's created upon McIDAS-V 
123         * starting and removed when McIDAS-V exits cleanly. This allows us to
124         * perform a primitive check to see if the current session has happened
125         * after a crash. 
126         */
127        private static String SESSION_FILE = getSessionFilePath();
128    
129        private static boolean cleanExit = true;
130    
131        private static Date previousStart = null;
132    
133        /** Set to true only if "-forceaqua" was found in the command line. */
134        public static boolean useAquaLookAndFeel = false;
135    
136        /** Points to the adde image defaults. */
137        public static final IdvResourceManager.XmlIdvResource RSC_FRAMEDEFAULTS =
138            new IdvResourceManager.XmlIdvResource("idv.resource.framedefaults",
139                               "McIDAS-X Frame Defaults");
140    
141        /** Points to the server definitions. */
142        public static final IdvResourceManager.XmlIdvResource RSC_SERVERS =
143            new IdvResourceManager.XmlIdvResource("idv.resource.servers",
144                               "Servers", "servers\\.xml$");
145    
146        /** Used to access McIDAS-V state in a static context. */
147        private static McIDASV staticMcv;
148    
149        /** Accessory in file save dialog */
150        private JCheckBox overwriteDataCbx = 
151            new JCheckBox("Change data paths", false);
152    
153        /** The chooser manager */
154        protected McIdasChooserManager chooserManager;
155    
156        /** The http based monitor to dump stack traces and shutdown the IDV */
157        private McIDASVMonitor mcvMonitor;
158    
159        /** {@link MonitorManager} allows for relatively easy and efficient monitoring of various resources. */
160        private final MonitorManager monitorManager = new MonitorManager();
161    
162        /** Actions passed into {@link #handleAction(String, Hashtable, boolean)}. */
163        private final List<String> actions = new LinkedList<String>();
164    
165        private enum WarningResult { OK, CANCEL, SHOW, HIDE };
166    
167        private EntryStore addeEntries;
168    
169        private TabbedAddeManager tabbedAddeManager = null;
170    
171        /**
172         * Create the McIDASV with the given command line arguments.
173         * This constructor calls {@link IntegratedDataViewer#init()}
174         * 
175         * @param args Command line arguments
176         * @exception VisADException  from construction of VisAd objects
177         * @exception RemoteException from construction of VisAD objects
178         */
179        public McIDASV(String[] args) throws VisADException, RemoteException {
180            super(args);
181    
182            AnnotationProcessor.process(this);
183    
184            staticMcv = this;
185    
186            // Keep this code around for reference--this requires MacMenuManager.java and MRJToolkit.
187            // We use OSXAdapter instead now, but it is unclear which is the preferred method.
188            // Let's use the one that works.
189    //        if (isMac()) {
190    //            try {
191    //                Object[] constructor_args = { this };
192    //                Class[] arglist = { McIDASV.class };
193    //                Class mac_class = Class.forName("edu.wisc.ssec.mcidasv.MacMenuManager");
194    //                Constructor new_one = mac_class.getConstructor(arglist);
195    //                new_one.newInstance(constructor_args);
196    //            }
197    //            catch(Exception e) {
198    //                System.out.println(e);
199    //            }
200    //        }
201    
202            // Set up our application to respond to the Mac OS X application menu
203            registerForMacOSXEvents();
204    
205            // This doesn't always look good... but keep it here for future reference
206    //        UIDefaults def = javax.swing.UIManager.getLookAndFeelDefaults();
207    //        Enumeration defkeys = def.keys();
208    //        while (defkeys.hasMoreElements()) {
209    //            Object item = defkeys.nextElement();
210    //            if (item.toString().indexOf("selectionBackground") > 0) {
211    //                def.put(item, Constants.MCV_BLUE);
212    //            }
213    //        }
214    
215            // we're tired of the IDV's default missing image, so reset it
216            GuiUtils.MISSING_IMAGE = "/edu/wisc/ssec/mcidasv/resources/icons/toolbar/mcidasv-round22.png";
217    
218            this.init();
219        }
220    
221        // Generic registration with the Mac OS X application menu
222        // Checks the platform, then attempts to register with the Apple EAWT
223        // See OSXAdapter.java to see how this is done without directly referencing any Apple APIs
224        public void registerForMacOSXEvents() {
225            if (isMac()) {
226                try {
227                    // Generate and register the OSXAdapter, passing it a hash of all the methods we wish to
228                    // use as delegates for various com.apple.eawt.ApplicationListener methods
229                    Class<?> thisClass = getClass();
230                    Class<?>[] args = (Class[])null;
231                    OSXAdapter.setQuitHandler(this, thisClass.getDeclaredMethod("MacOSXQuit", args));
232                    OSXAdapter.setAboutHandler(this, thisClass.getDeclaredMethod("MacOSXAbout", args));
233                    OSXAdapter.setPreferencesHandler(this, thisClass.getDeclaredMethod("MacOSXPreferences", args));
234                } catch (Exception e) {
235                    logger.error("Error while loading the OSXAdapter", e);
236                }
237            }
238        }
239    
240        public boolean MacOSXQuit() {
241            return quit();
242        }
243    
244        public void MacOSXAbout() {
245            getIdvUIManager().about();
246        }
247    
248        public void MacOSXPreferences() {
249            showPreferenceManager();
250        }
251    
252        /**
253         * Start up the McIDAS-V monitor server. This is an http server on the port defined
254         * by the property idv.monitorport (8788).  It is only accessible to 127.0.0.1 (localhost)
255         */
256        // TODO: we probably don't want our own copy of this in the long run...
257        // all we did was change "IDV" to "McIDAS-V"
258        @Override protected void startMonitor() {
259            if (mcvMonitor != null) {
260                return;
261            }
262            final String monitorPort = getProperty(PROP_MONITORPORT, "");
263            if (monitorPort!=null && monitorPort.trim().length()>0 && !"none".equals(monitorPort.trim())) {
264                Misc.run(new Runnable() {
265                    @Override public void run() {
266                        try {
267                            mcvMonitor = new McIDASVMonitor(McIDASV.this, Integer.parseInt(monitorPort));
268                            mcvMonitor.init();
269                        } catch (Exception exc) {
270                            LogUtil.consoleMessage("Unable to start McIDAS-V monitor on port:" + monitorPort);
271                            LogUtil.consoleMessage("Error:" + exc);
272                        }
273                    }
274                });
275            }
276        }
277        
278        /**
279         * Initializes a XML encoder with McIDAS-V specific XML delegates.
280         * 
281         * @param encoder XML encoder that'll be dealing with persistence.
282         * @param forRead Not used as of yet.
283         */
284        // TODO: if we ever get up past three or so XML delegates, I vote that we
285        // make our own version of VisADPersistence.
286        @Override protected void initEncoder(XmlEncoder encoder, boolean forRead) {
287    
288            encoder.addDelegateForClass(LambertAEA.class, new XmlDelegateImpl() {
289                @Override public Element createElement(XmlEncoder e, Object o) {
290                    LambertAEA projection = (LambertAEA)o;
291                    Rectangle2D rect = projection.getDefaultMapArea();
292                    List args = Misc.newList(rect);
293                    List types = Misc.newList(rect.getClass());
294                    return e.createObjectConstructorElement(o, args, types);
295                }
296            });
297    
298            // TODO(jon): ultra fashion makeover!!
299            encoder.addDelegateForClass(RemoteAddeEntry.class, new XmlDelegateImpl() {
300                @Override public Element createElement(XmlEncoder e, Object o) {
301                    RemoteAddeEntry entry = (RemoteAddeEntry)o;
302                    Element element = e.createObjectElement(o.getClass());
303                    element.setAttribute("address", entry.getAddress());
304                    element.setAttribute("group", entry.getGroup());
305                    element.setAttribute("username", entry.getAccount().getUsername());
306                    element.setAttribute("project", entry.getAccount().getProject());
307                    element.setAttribute("source", entry.getEntrySource().toString());
308                    element.setAttribute("type", entry.getEntryType().toString());
309                    element.setAttribute("validity", entry.getEntryValidity().toString());
310                    element.setAttribute("status", entry.getEntryStatus().toString());
311                    return element;
312                }
313    
314                @Override public Object createObject(XmlEncoder e, Element element) {
315                    String address = getAttribute(element, "address");
316                    String group = getAttribute(element, "group");
317                    String username = getAttribute(element, "username");
318                    String project = getAttribute(element, "project");
319                    String source = getAttribute(element, "source");
320                    String type = getAttribute(element, "type");
321                    String validity = getAttribute(element, "validity");
322                    String status = getAttribute(element, "status");
323    
324                    EntrySource entrySource = EntryTransforms.strToEntrySource(source);
325                    EntryType entryType = EntryTransforms.strToEntryType(type);
326                    EntryValidity entryValidity = EntryTransforms.strToEntryValidity(validity);
327                    EntryStatus entryStatus = EntryTransforms.strToEntryStatus(status);
328    
329                    RemoteAddeEntry entry = 
330                        new RemoteAddeEntry.Builder(address, group)
331                            .account(username, project)
332                            .source(entrySource)
333                            .type(entryType)
334                            .validity(entryValidity)
335                            .status(entryStatus).build();
336    
337                    return entry;
338                }
339            });
340    
341            encoder.addDelegateForClass(LocalAddeEntry.class, new XmlDelegateImpl() {
342                @Override public Element createElement(XmlEncoder e, Object o) {
343                    LocalAddeEntry entry = (LocalAddeEntry)o;
344                    Element element = e.createObjectElement(o.getClass());
345                    element.setAttribute("group", entry.getGroup());
346                    element.setAttribute("descriptor", entry.getDescriptor());
347                    element.setAttribute("realtime", entry.getRealtimeAsString());
348                    element.setAttribute("format", entry.getFormat().name());
349                    element.setAttribute("start", entry.getStart());
350                    element.setAttribute("end", entry.getEnd());
351                    element.setAttribute("fileMask", entry.getMask());
352                    element.setAttribute("name", entry.getName());
353                    element.setAttribute("status", entry.getEntryStatus().name());
354                    return element;
355                }
356                @Override public Object createObject(XmlEncoder e, Element element) {
357                    String group = getAttribute(element, "group");
358                    String descriptor = getAttribute(element, "descriptor");
359                    String realtime = getAttribute(element, "realtime", "");
360                    AddeFormat format = EntryTransforms.strToAddeFormat(XmlUtil.getAttribute(element, "format"));
361                    String start = getAttribute(element, "start", "1");
362                    String end = getAttribute(element, "end", "999999");
363                    String fileMask = getAttribute(element, "fileMask");
364                    String name = getAttribute(element, "name");
365                    String status = getAttribute(element, "status", "ENABLED");
366                    LocalAddeEntry.Builder builder = new LocalAddeEntry.Builder(name, group, fileMask, format).range(start, end).descriptor(descriptor).realtime(realtime).status(status);
367                    return builder.build();
368                }
369            });
370            
371    //        encoder.addHighPriorityDelegateForClass(AddeImageInfo.class, new XmlDelegateImpl() {
372    //            @Override public Element createElement(XmlEncoder e, Object o) {
373    //                AddeImageInfo info = (AddeImageInfo)o;
374    //                String user = info.getUser();
375    //                int proj = info.getProject();
376    //                logger.trace("user={} proj={}", new Object[] { user, proj });
377    //                return e.createElementDontCheckDelegate(o);
378    //            }
379    //            @Override public Object createObject(XmlEncoder e, Element element) {
380    //                String host = getAttribute(element, "Host");
381    //                String group = getAttribute(element, "Group");
382    //                String descriptor = getAttribute(element, "Descriptor");
383    //                String type = getAttribute(element, "RequestType");
384    //                
385    //                EntryStore store = getServerManager();
386    //                boolean mcservRunning = store.checkLocalServer();
387    //                boolean isKnown = store.searchWithPrefix(host+'!'+group).isEmpty();
388    //                
389    //                logger.trace("isKnown={} host='{}' group='{}' type='{}' desc='{}'", new Object[] { isKnown, host, group, descriptor, type });
390    //                return e.createObjectDontCheckDelegate(element);
391    //            }
392    //        });
393            
394    //        encoder.addHighPriorityDelegateForClass(AddeImageDescriptor.class, new XmlDelegateImpl() {
395    //            @Override public Element createElement(XmlEncoder e, Object o) {
396    //                AddeImageDescriptor desc = (AddeImageDescriptor)o;
397    //                String source = desc.getSource();
398    //                desc.setSource(source.replace("USER", "user"));
399    //                return desc.createElement(e, o);
400    //            }
401    //            @Override public Object createObject(XmlEncoder e, Element element) {
402    //                
403    //                return e.createObjectDontCheckDelegate(element);
404    //            }
405    //        });
406            
407            /**
408             * Move legacy classes to a new location
409             */
410            encoder.registerNewClassName("edu.wisc.ssec.mcidasv.data.Test2ImageDataSource",
411                            "edu.wisc.ssec.mcidasv.data.adde.AddeImageParameterDataSource");
412            encoder.registerNewClassName("edu.wisc.ssec.mcidasv.data.Test2AddeImageDataSource",
413                    "edu.wisc.ssec.mcidasv.data.adde.AddeImageParameterDataSource");
414            encoder.registerNewClassName("edu.wisc.ssec.mcidasv.data.AddePointDataSource",
415                            "edu.wisc.ssec.mcidasv.data.adde.AddePointDataSource");
416            encoder.registerNewClassName("edu.wisc.ssec.mcidasv.data.AddeSoundingAdapter",
417                            "edu.wisc.ssec.mcidasv.data.adde.AddeSoundingAdapter");
418        }
419    
420        /**
421         * Returns <i>all</i> of the actions used in this McIDAS-V session. This is
422         * possibly TMI and might be removed...
423         * 
424         * @return Actions executed thus far.
425         */
426        public List<String> getActionHistory() {
427            return actions;
428        }
429    
430        /**
431         * Converts {@link ArgsManager#getOriginalArgs()} to a {@link List} and
432         * returns.
433         * 
434         * @return The command-line arguments used to start McIDAS-V, as an 
435         * {@code ArrayList}.
436         */
437        public List<String> getCommandLineArgs() {
438            String[] originalArgs = getArgsManager().getOriginalArgs();
439            List<String> args = arrList(originalArgs.length);
440            Collections.addAll(args, originalArgs);
441            return args;
442        }
443    
444        /**
445         * Captures the action passed to {@code handleAction}. The action is logged
446         * and additionally, if the action is a HTML link, we attempt to visit the
447         * link in the user's preferred browser.
448         */
449        @Override public boolean handleAction(String action, Hashtable properties, 
450            boolean checkForAlias) 
451        {
452            actions.add(action);
453    
454            if (IOUtil.isHtmlFile(action)) {
455                WebBrowser.browse(action);
456                return true;
457            }
458    
459            return super.handleAction(action, properties, checkForAlias);
460        }
461    
462        /**
463         *  This method checks if the given action is one of the following.
464         * <p>
465         *  <li> Jython code -- starts with jython:<br>
466         *  <li> Help link -- starts with help:<br>
467         *  <li> Resource bundle file -- ends with .rbi<br>
468         *  <li> bundle file -- ends with .xidv<br>
469         *  <li> jnlp file -- ends with .jnlp<br>
470         *  It returns true if the action is one of these.
471         *  False otherwise.
472         *
473         * @param action The string action
474         * @param properties any properties
475         * @return Was this action handled
476         */
477        @Override protected boolean handleFileOrUrlAction(String action, Hashtable properties) {
478            boolean result = false;
479            boolean idvAction = action.startsWith("idv:");
480            boolean jythonAction = action.startsWith("jython:");
481    
482            if (!idvAction && !jythonAction) {
483                return super.handleFileOrUrlAction(action, properties);
484            }
485    
486            Map<String, Object> hashProps;
487            if (properties != null) {
488                hashProps = new HashMap<String, Object>(properties);
489            } else {
490                //noinspection CollectionWithoutInitialCapacity
491                hashProps = new HashMap<String, Object>();
492            }
493    
494            ucar.unidata.idv.JythonManager jyManager = getJythonManager();
495            if (idvAction) {
496                action = new String(action.replace("&", "&amp;").substring(4));
497    //            getJythonManager().evaluateUntrusted(action, hashProps);
498                jyManager.evaluateUntrusted(action, hashProps);
499                result = true;
500            } else if (jythonAction) {
501                action = new String(action.substring(7));
502                jyManager.evaluateTrusted(action, hashProps);
503    //            ucar.unidata.idv.JythonManager jyMan = new ucar.unidata.idv.JythonManager();
504    //            jyMan
505    //            getJythonManager().evaluateTrusted(action, hashProps);
506                result = true;
507            } else {
508                result = super.handleFileOrUrlAction(action, properties);
509            }
510            return result;
511        }
512    
513        /**
514         * Add a new {@link ControlDescriptor} into the {@code controlDescriptor}
515         * list and {@code controlDescriptorMap}.
516         * 
517         * <p>This method differs from the IDV's in that McIDAS-V <b>overwrites</b>
518         * existing {@code ControlDescriptor}s if 
519         * {@link ControlDescriptor#getControlId()} matches.
520         * 
521         * @param cd The ControlDescriptor to add.
522         * 
523         * @throws NullPointerException if {@code cd} is {@code null}.
524         */
525        @Override protected void addControlDescriptor(ControlDescriptor cd) {
526            Contract.notNull(cd, "Cannot add a null control descriptor to the list of control descriptors");
527            String id = cd.getControlId();
528            if (controlDescriptorMap.get(id) == null) {
529                controlDescriptors.add(cd);
530                controlDescriptorMap.put(id, cd);
531            } else {
532                for (int i = 0; i < controlDescriptors.size(); i++) {
533                    ControlDescriptor tmp = (ControlDescriptor)controlDescriptors.get(i);
534                    if (tmp.getControlId().equals(id)) {
535                        controlDescriptors.set(i, cd);
536                        controlDescriptorMap.put(id, cd);
537                        break;
538                    }
539                }
540            }
541        }
542    
543        // pop up an incredibly rudimentary window that controls layer viz animation.
544        public void showLayerVisibilityAnimator() {
545            logger.trace("probably should try to do something here.");
546            SwingUtilities.invokeLater(new Runnable() {
547                public void run() {
548                    try {
549                        LayerAnimationWindow window = new LayerAnimationWindow();
550                        window.setVisible(true);
551                    } catch (Exception e) {
552                        logger.error("oh no! something happened!", e);
553                    }
554                }
555            });
556        }
557    
558        /**
559         * Handles removing all loaded data sources.
560         * 
561         * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method
562         * will ignore the user's preferences and remove all data sources.
563         * 
564         * @param showWarning Whether or not to display a warning message before
565         * removing <i>all</i> data sources. See the return details for more.
566         * 
567         * @return Either {@code true} if the user wants to continue showing the
568         * warning dialog, or {@code false} if they've elected to stop showing the
569         * warning. If {@code showWarning} is {@code false}, this method will 
570         * always return {@code false}, as the user isn't interested in seeing the
571         * warning.
572         */
573        public boolean removeAllData(final boolean showWarning) {
574            boolean reallyRemove = false;
575            boolean continueWarning = true;
576    
577            if (getArgsManager().getIsOffScreen()) {
578                super.removeAllDataSources();
579                return continueWarning;
580            }
581    
582            if (showWarning) {
583                Set<WarningResult> result = showWarningDialog(
584                    "Confirm Data Removal",
585                    "This action will remove all of the data currently loaded in McIDAS-V.<br>Is this what you want to do?",
586                    Constants.PREF_CONFIRM_REMOVE_DATA,
587                    "Always ask?",
588                    "Remove all data",
589                    "Do not remove any data");
590                reallyRemove = result.contains(WarningResult.OK);
591                continueWarning = result.contains(WarningResult.SHOW);
592            } else {
593                // user doesn't want to see warning messages.
594                reallyRemove = true;
595                continueWarning = false;
596            }
597    
598            if (reallyRemove) {
599                super.removeAllDataSources();
600            }
601    
602            return continueWarning;
603        }
604    
605        /**
606         * Handles removing all loaded layers ({@literal "displays"} in IDV-land).
607         * 
608         * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method
609         * will ignore the user's preferences and remove all layers.
610         * 
611         * @param showWarning Whether or not to display a warning message before
612         * removing <i>all</i> layers. See the return details for more.
613         * 
614         * @return Either {@code true} if the user wants to continue showing the
615         * warning dialog, or {@code false} if they've elected to stop showing the
616         * warning. If {@code showWarning} is {@code false}, this method will 
617         * always return {@code false}, as the user isn't interested in seeing the
618         * warning.
619         */
620        public boolean removeAllLayers(final boolean showWarning) {
621            boolean reallyRemove = false;
622            boolean continueWarning = true;
623    
624            if (getArgsManager().getIsOffScreen()) {
625                super.removeAllDisplays();
626                ((ViewManagerManager)getVMManager()).disableAllLayerVizAnimations();
627                return continueWarning;
628            }
629    
630            if (showWarning) {
631                Set<WarningResult> result = showWarningDialog(
632                    "Confirm Layer Removal",
633                    "This action will remove every layer currently loaded in McIDAS-V.<br>Is this what you want to do?",
634                    Constants.PREF_CONFIRM_REMOVE_LAYERS,
635                    "Always ask?",
636                    "Remove all layers",
637                    "Do not remove any layers");
638                reallyRemove = result.contains(WarningResult.OK);
639                continueWarning = result.contains(WarningResult.SHOW);
640            } else {
641                // user doesn't want to see warning messages.
642                reallyRemove = true;
643                continueWarning = false;
644            }
645    
646            if (reallyRemove) {
647                super.removeAllDisplays();
648                ((ViewManagerManager)getVMManager()).disableAllLayerVizAnimations();
649            }
650    
651            return continueWarning;
652        }
653    
654        /**
655         * Overridden so that McIDAS-V can prompt the user before removing, if 
656         * necessary.
657         */
658        @Override public void removeAllDataSources() {
659            IdvObjectStore store = getStore();
660            boolean showWarning = store.get(Constants.PREF_CONFIRM_REMOVE_DATA, true);
661            showWarning = removeAllData(showWarning);
662            store.put(Constants.PREF_CONFIRM_REMOVE_DATA, showWarning);
663        }
664    
665        /**
666         * Overridden so that McIDAS-V can prompt the user before removing, if 
667         * necessary.
668         */
669        @Override public void removeAllDisplays() {
670            IdvObjectStore store = getStore();
671            boolean showWarning = store.get(Constants.PREF_CONFIRM_REMOVE_LAYERS, true);
672            showWarning = removeAllLayers(showWarning);
673            store.put(Constants.PREF_CONFIRM_REMOVE_LAYERS, showWarning);
674        }
675    
676        /**
677         * Handles removing all loaded layers ({@literal "displays"} in IDV-land)
678         * and data sources. 
679         * 
680         * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method
681         * will ignore the user's preferences and remove all layers and data.
682         * 
683         * @see #removeAllData(boolean)
684         * @see #removeAllLayers(boolean)
685         */
686        public void removeAllLayersAndData() {
687            boolean reallyRemove = false;
688            boolean continueWarning = true;
689    
690            if (getArgsManager().getIsOffScreen()) {
691                removeAllData(false);
692                removeAllLayers(false);
693            }
694    
695            IdvObjectStore store = getStore();
696            boolean showWarning = store.get(Constants.PREF_CONFIRM_REMOVE_BOTH, true);
697            if (showWarning) {
698                Set<WarningResult> result = showWarningDialog(
699                    "Confirm Removal",
700                    "This action will remove all of your currently loaded layers and data.<br>Is this what you want to do?",
701                    Constants.PREF_CONFIRM_REMOVE_BOTH,
702                    "Always ask?",
703                    "Remove all layers and data",
704                    "Do not remove anything");
705                reallyRemove = result.contains(WarningResult.OK);
706                continueWarning = result.contains(WarningResult.SHOW);
707            } else {
708                // user doesn't want to see warning messages.
709                reallyRemove = true;
710                continueWarning = false;
711            }
712    
713            // don't show the individual warning messages as the user has attempted
714            // to remove *both*
715            if (reallyRemove) {
716                removeAllData(false);
717                removeAllLayers(false);
718            }
719    
720            store.put(Constants.PREF_CONFIRM_REMOVE_BOTH, continueWarning);
721        }
722    
723        /**
724         * Helper method for showing the removal warning dialog. Note that none of
725         * these parameters should be {@code null} or empty.
726         * 
727         * @param title Title of the warning dialog.
728         * @param message Contents of the warning. May contain HTML, but you do 
729         * not need to provide opening and closing {@literal "html"} tags.
730         * @param prefId ID of the preference that controls whether or not the 
731         * dialog should be displayed.
732         * @param prefLabel Brief description of the preference.
733         * @param okLabel Text of button that signals removal.
734         * @param cancelLabel Text of button that signals cancelling removal.
735         * 
736         * @return A {@code Set} of {@link WarningResult}s that describes what the
737         * user opted to do. Should always contain only <b>two</b> elements. One
738         * for whether or not {@literal "ok"} or {@literal "cancel"} was clicked,
739         * and one for whether or not the warning should continue to be displayed.
740         */
741        private Set<WarningResult> showWarningDialog(final String title, 
742            final String message, final String prefId, final String prefLabel, 
743            final String okLabel, final String cancelLabel) 
744        {
745            JCheckBox box = new JCheckBox(prefLabel, true);
746            JComponent comp = GuiUtils.vbox(
747                new JLabel("<html>"+message+"</html>"), 
748                GuiUtils.inset(box, new Insets(4, 15, 0, 10)));
749    
750            Object[] options = { okLabel, cancelLabel };
751            int result = JOptionPane.showOptionDialog(
752                LogUtil.getCurrentWindow(),  // parent
753                comp,                        // msg
754                title,                       // title
755                JOptionPane.YES_NO_OPTION,   // option type
756                JOptionPane.WARNING_MESSAGE, // message type
757                (Icon)null,                  // icon?
758                options,                     // selection values
759                options[1]);                 // initial?
760    
761            WarningResult button = WarningResult.CANCEL;
762            if (result == JOptionPane.YES_OPTION) {
763                button = WarningResult.OK;
764            }
765    
766            WarningResult show = WarningResult.HIDE;
767            if (box.isSelected()) {
768                show = WarningResult.SHOW;
769            }
770    
771            return EnumSet.of(button, show);
772        }
773    
774        public void removeTabData() {
775        }
776    
777        public void removeTabLayers() {
778            
779        }
780    
781        public void removeTabLayersAndData() {
782        }
783    
784        /**
785         * Overridden so that McIDAS-V doesn't have to create an entire new {@link ucar.unidata.idv.ui.IdvWindow}
786         * if {@link VMManager#findViewManager(ViewDescriptor)} can't find an appropriate
787         * ViewManager for {@code viewDescriptor}.
788         * 
789         * <p>Not doing the above causes McIDAS-V to get stuck in a window creation
790         * loop.
791         */
792        @Override public ViewManager getViewManager(ViewDescriptor viewDescriptor,
793            boolean newWindow, String properties) 
794        {
795            ViewManager vm = 
796                getVMManager().findOrCreateViewManager(viewDescriptor, properties);
797            if (vm == null) {
798                vm = super.getViewManager(viewDescriptor, newWindow, properties);
799            }
800            return vm;
801        }
802    
803        /**
804         * Returns a reference to the current McIDAS-V object. Useful for working 
805         * inside static methods. <b>Always check for null when using this 
806         * method</b>.
807         * 
808         * @return Either the current McIDAS-V "god object" or {@code null}.
809         */
810        public static McIDASV getStaticMcv() {
811            return staticMcv;
812        }
813    
814        /**
815         * @see ucar.unidata.idv.IdvBase#setIdv(ucar.unidata.idv.IntegratedDataViewer)
816         */
817        @Override
818        public void setIdv(IntegratedDataViewer idv) {
819            this.idv = idv;
820        }
821    
822        /**
823         * Load the McV properties. All other property files are disregarded.
824         * 
825         * @see ucar.unidata.idv.IntegratedDataViewer#initPropertyFiles(java.util.List)
826         */
827        @Override
828        public void initPropertyFiles(List files) {
829            files.clear();
830            files.add(Constants.PROPERTIES_FILE);
831        }
832    
833        /**
834         * Makes {@link PersistenceManager} save off a default {@literal "layout"}
835         * bundle.
836         */
837        public void doSaveAsDefaultLayout() {
838            Misc.run(new Runnable() {
839                @Override public void run() {
840                    ((PersistenceManager)getPersistenceManager()).doSaveAsDefaultLayout();
841                }
842            });
843        }
844    
845        /**
846         * Determines whether or not a default layout exists.
847         * 
848         * @return {@code true} if there is a default layout, {@code false} 
849         * otherwise.
850         */
851        public boolean hasDefaultLayout() {
852            String path = 
853                getResourceManager().getResources(IdvResourceManager.RSC_BUNDLES)
854                    .getWritable();
855            return new File(path).exists();
856        }
857    
858        /**
859         * Called from the menu command to clear the default bundle. Overridden
860         * in McIDAS-V so that we reference the <i>layout</i> rather than the
861         * bundle.
862         */
863        @Override public void doClearDefaults() {
864            if (GuiUtils.showYesNoDialog(null, 
865                    "Are you sure you want to delete your default layout?",
866                    "Delete confirmation")) 
867                resourceManager.clearDefaultBundles();
868        }
869    
870        /**
871         * <p>
872         * Overridden so that the support form becomes non-modal if launched from
873         * an exception dialog.
874         * </p>
875         * 
876         * @see IntegratedDataViewer#addErrorButtons(JDialog, List, String, Throwable)
877         */
878        @Override public void addErrorButtons(final JDialog dialog, 
879            List buttonList, final String msg, final Throwable exc) 
880        {
881            JButton supportBtn = new JButton("Support Form");
882            supportBtn.addActionListener(new ActionListener() {
883                @Override public void actionPerformed(ActionEvent ae) {
884                    getIdvUIManager().showSupportForm(msg,
885                        LogUtil.getStackTrace(exc), null);
886                }
887            });
888            buttonList.add(supportBtn);
889        }
890    
891        /**
892         * Called after the IDV has finished setting everything up after starting.
893         * McIDAS-V is currently only using this method to determine if the last
894         * {@literal "exit"} was clean--whether or not {@code SESSION_FILE} was 
895         * removed before the McIDAS-V process terminated.
896         */
897        @Override public void initDone() {
898            super.initDone();
899            GuiUtils.setApplicationTitle("");
900            if (cleanExit || getArgsManager().getIsOffScreen()) {
901                return;
902            }
903    
904            String msg = "The previous McIDAS-V session did not exit cleanly.<br>"+ 
905                         "Do you want to send the log file to the McIDAS Help Desk?";
906            if (previousStart != null) {
907                msg = "The previous McIDAS-V session (start time: %s) did not exit cleanly.<br>"+ 
908                      "Do you want to send the log file to the McIDAS Help Desk?";
909                msg = String.format(msg, previousStart);
910            }
911    
912            boolean continueAsking = getStore().get("mcv.crash.boom.send.report", true);
913            if (!continueAsking) {
914                return;
915            }
916    
917            Set<WarningResult> result = showWarningDialog(
918                    "Report Crash",
919                    msg,
920                    "mcv.crash.boom.send.report",
921                    "Always ask?",
922                    "Open support form",
923                    "Do not report");
924    
925            getStore().put("mcv.crash.boom.send.report", result.contains(WarningResult.SHOW));
926            if (!result.contains(WarningResult.OK)) {
927                return;
928            }
929    
930            getIdvUIManager().showSupportForm();
931        }
932    
933        /**
934         * @see IntegratedDataViewer#doOpen(String, boolean, boolean)
935         */
936        @Override public void doOpen(final String filename,
937            final boolean checkUserPreference, final boolean andRemove) 
938        {
939            doOpenInThread(filename, checkUserPreference, andRemove);
940        }
941    
942        /**
943         * Have the user select an xidv file. If andRemove is true then we remove
944         * all data sources and displays. Then we open the unpersist the bundle in
945         * the xidv file
946         * 
947         * @param filename The filename to open
948         * @param checkUserPreference Should we show, if needed, the Open dialog
949         * @param andRemove If true then first remove all data sources and displays
950         */
951        private void doOpenInThread(String filename, boolean checkUserPreference,
952            boolean andRemove) 
953        {
954            boolean overwriteData = false;
955            if (filename == null) {
956                if (overwriteDataCbx.getToolTipText() == null) {
957                    overwriteDataCbx.setToolTipText("Change the file paths that the data sources use");
958                }
959    
960                filename = FileManager.getReadFile("Open File",
961                    ((ArgumentManager)getArgsManager()).getBundleFilters(true), 
962                    GuiUtils.top(overwriteDataCbx));
963    
964                if (filename == null) {
965                    return;
966                }
967    
968                overwriteData = overwriteDataCbx.isSelected();
969            }
970    
971            if (ArgumentManager.isXmlBundle(filename)) {
972                getPersistenceManager().decodeXmlFile(filename,
973                    checkUserPreference, overwriteData);
974                return;
975            }
976            handleAction(filename, null);
977        }
978    
979        /**
980         * Make edu.wisc.ssec.mcidasv.JythonManager
981         * Factory method to create the
982         * {@link JythonManager}
983         *
984         * @return The  jython manager
985         */
986        @Override protected JythonManager doMakeJythonManager() {
987    //        return (JythonManager) makeManager(JythonManager.class,
988    //                                           new Object[] { idv });
989            logger.debug("returning a new JythonManager");
990            return new JythonManager(this);
991        }
992    
993        /**
994         * Factory method to create the {@link IdvUIManager}. Here we create our 
995         * own UI manager so it can do McV specific things.
996         *
997         * @return The UI manager indicated by the startup properties.
998         * 
999         * @see ucar.unidata.idv.IdvBase#doMakeIdvUIManager()
1000         */
1001        @Override
1002        protected IdvChooserManager doMakeIdvChooserManager() {
1003            chooserManager = (McIdasChooserManager)makeManager(
1004                McIdasChooserManager.class, new Object[] { this });
1005            chooserManager.init();
1006            return chooserManager;
1007        }
1008    
1009        /**
1010         * Factory method to create the {@link IdvUIManager}. Here we create our 
1011         * own UI manager so it can do McV specific things.
1012         *
1013         * @return The UI manager indicated by the startup properties.
1014         * 
1015         * @see ucar.unidata.idv.IdvBase#doMakeIdvUIManager()
1016         */
1017        @Override
1018        protected IdvUIManager doMakeIdvUIManager() {
1019            return new UIManager(this);
1020        }
1021    
1022        /**
1023         * Create our own VMManager so that we can make the tabs play nice.
1024         * @see ucar.unidata.idv.IdvBase#doMakeVMManager()
1025         */
1026        @Override
1027        protected VMManager doMakeVMManager() {
1028            // what an ugly class name :(
1029            return new ViewManagerManager(this);
1030        }
1031    
1032        /**
1033         * Make the {@link McIdasPreferenceManager}.
1034         * @see ucar.unidata.idv.IdvBase#doMakePreferenceManager()
1035         */
1036        @Override
1037        protected IdvPreferenceManager doMakePreferenceManager() {
1038            return new McIdasPreferenceManager(this);
1039        }
1040    
1041        /**
1042         * <p>McIDAS-V (alpha 10+) needs to handle both IDV bundles without 
1043         * component groups and all bundles from prior McV alphas. You better 
1044         * believe we need to extend the persistence manager functionality!</p>
1045         * 
1046         * @see ucar.unidata.idv.IdvBase#doMakePersistenceManager()
1047         */
1048        @Override protected IdvPersistenceManager doMakePersistenceManager() {
1049            return new PersistenceManager(this);
1050        }
1051    
1052        /**
1053         * Create, if needed, and return the {@link McIdasChooserManager}.
1054         * 
1055         * @return The Chooser manager
1056         */
1057        public McIdasChooserManager getMcIdasChooserManager() {
1058            return (McIdasChooserManager)getIdvChooserManager();
1059        }
1060    
1061        /**
1062         * Returns the {@link MonitorManager}.
1063         */
1064        public MonitorManager getMonitorManager() {
1065            return monitorManager;
1066        }
1067    
1068        /**
1069         * Responds to events generated by the server manager's GUI. Currently
1070         * limited to {@link TabbedAddeManager.Event#CLOSED}.
1071         */
1072        @EventSubscriber(eventClass=TabbedAddeManager.Event.class)
1073        public void onServerManagerWindowEvent(TabbedAddeManager.Event evt) {
1074            if (evt == TabbedAddeManager.Event.CLOSED) {
1075                tabbedAddeManager = null;
1076            }
1077        }
1078    
1079        /**
1080         * Creates (if needed) the server manager GUI and displays it.
1081         */
1082        public void showServerManager() {
1083            if (tabbedAddeManager == null) {
1084                tabbedAddeManager = new TabbedAddeManager(getServerManager());
1085            }
1086            tabbedAddeManager.showManager();
1087        }
1088    
1089        /**
1090         * Creates a new server manager (if needed) and returns it.
1091         */
1092        public EntryStore getServerManager() {
1093            if (addeEntries == null) {
1094                addeEntries = new EntryStore(getStore(), getResourceManager());
1095                addeEntries.startLocalServer();
1096            }
1097            return addeEntries;
1098        }
1099    
1100        public McvDataManager getMcvDataManager() {
1101            return (McvDataManager)getDataManager();
1102        }
1103    
1104        /**
1105         * Get McIDASV. 
1106         * @see ucar.unidata.idv.IdvBase#getIdv()
1107         */
1108        @Override public IntegratedDataViewer getIdv() {
1109            return this;
1110        }
1111    
1112        /**
1113         * Creates a McIDAS-V argument manager so that McV can handle some non-IDV
1114         * command line things.
1115         * 
1116         * @param args The arguments from the command line.
1117         * 
1118         * @see ucar.unidata.idv.IdvBase#doMakeArgsManager(java.lang.String[])
1119         */
1120        @Override protected ArgsManager doMakeArgsManager(String[] args) {
1121            return new ArgumentManager(this, args);
1122        }
1123    
1124        /**
1125         * Factory method to create the {@link McvDataManager}.
1126         * 
1127         * @return The data manager
1128         * 
1129         * @see ucar.unidata.idv.IdvBase#doMakeDataManager()
1130         */
1131        @Override protected DataManager doMakeDataManager() {
1132            return new McvDataManager(this);
1133        }
1134    
1135        /**
1136         * Make the McIDAS-V {@link StateManager}.
1137         * @see ucar.unidata.idv.IdvBase#doMakeStateManager()
1138         */
1139        @Override protected StateManager doMakeStateManager() {
1140            return new StateManager(this);
1141        }
1142    
1143        /**
1144         * Make the McIDAS-V {@link ResourceManager}.
1145         * @see ucar.unidata.idv.IdvBase#doMakeResourceManager()
1146         */
1147        @Override protected IdvResourceManager doMakeResourceManager() {
1148            return new ResourceManager(this);
1149        }
1150    
1151        /**
1152         * Make the {@link McIdasColorTableManager}.
1153         * @see ucar.unidata.idv.IdvBase#doMakeColorTableManager()
1154         */
1155        @Override protected ColorTableManager doMakeColorTableManager() {
1156            return new McIdasColorTableManager();
1157        }
1158    
1159        /**
1160         * Factory method to create the {@link McvPluginManager}.
1161         *
1162         * @return The McV plugin manager.
1163         * 
1164         * @see ucar.unidata.idv.IdvBase#doMakePluginManager()
1165         */
1166        @Override protected PluginManager doMakePluginManager() {
1167            return new McvPluginManager(this);
1168        }
1169    
1170    //    /**
1171    //     * Make the {@link edu.wisc.ssec.mcidasv.data.McIDASVProjectionManager}.
1172    //     * @see ucar.unidata.idv.IdvBase#doMakeIdvProjectionManager()
1173    //     */
1174    //    @Override
1175    //    protected IdvProjectionManager doMakeIdvProjectionManager() {
1176    //      return new McIDASVProjectionManager(this);
1177    //    }
1178        
1179        /**
1180         * Make a help button for a particular help topic
1181         *
1182         * @param helpId  the topic id
1183         * @param toolTip  the tooltip
1184         *
1185         * @return  the button
1186         */
1187        @Override public JComponent makeHelpButton(String helpId, String toolTip) {
1188            JButton btn = McVGuiUtils.makeImageButton(Constants.ICON_HELP,
1189                getIdvUIManager(), "showHelp", helpId, "Show help");
1190    
1191            if (toolTip != null) {
1192                btn.setToolTipText(toolTip);
1193            }
1194            return btn;
1195        }
1196    
1197        /**
1198         * Return the current {@literal "userpath"}
1199         */
1200        public String getUserDirectory() {
1201            return StartupManager.INSTANCE.getPlatform().getUserDirectory();
1202        }
1203    
1204        /**
1205         * Return the path to a file within {@literal "userpath"}
1206         */
1207        public String getUserFile(String filename) {
1208            return StartupManager.INSTANCE.getPlatform().getUserFile(filename);
1209        }
1210    
1211        /**
1212         * Invokes the main method for a given class. 
1213         * 
1214         * <p>Note: this is rather limited so far as it doesn't pass in any arguments.
1215         * 
1216         * @param className Class whose main method is to be invoked. Cannot be {@code null}.
1217         */
1218        public void runPluginMainMethod(final String className) {
1219            final String[] dummyArgs = { "" };
1220            try {
1221                Class<?> clazz = Misc.findClass(className);
1222                Method mainMethod = Misc.findMethod(clazz, "main", new Class[] { dummyArgs.getClass() });
1223                if (mainMethod != null) {
1224                    mainMethod.invoke(null, new Object[] { dummyArgs });
1225                }
1226            } catch (Exception e) {
1227                logger.error("problem with plugin class", e);
1228                LogUtil.logException("problem running main method for class: "+className, e);
1229            }
1230        }
1231    
1232        /**
1233         * Attempts to determine if a given string is a 
1234         * {@literal "loopback address"} (aka localhost).
1235         * 
1236         * <p>Strings are <b>trimmed and converted to lowercase</b>, and currently
1237         * checked against:
1238         * <ul>
1239         * <li>{@code 127.0.0.1}</li>
1240         * <li>{@code ::1} (for IPv6)</li>
1241         * <li>Strings starting with {@code localhost}.</li>
1242         * </ul>
1243         * 
1244         * @param host {@code String} to check. Should not be {@code null}.
1245         * 
1246         * @return {@code true} if {@code host} is a recognized loopback address.
1247         * {@code false} otherwise.
1248         * 
1249         * @throws NullPointerException if {@code host} is {@code null}.
1250         */
1251        public static boolean isLoopback(final String host) {
1252            String cleaned = Contract.notNull(host.trim().toLowerCase());
1253            return "127.0.0.1".startsWith(cleaned) 
1254                || "::1".startsWith(cleaned) 
1255                || cleaned.startsWith("localhost");
1256        }
1257    
1258        /**
1259         * Are we on a Mac?  Used to build the MRJ handlers
1260         * Take from TN2042
1261         * @return
1262         */
1263        public static boolean isMac() {
1264            return System.getProperty("mrj.version") != null;
1265        }
1266    
1267        /**
1268         * Queries the {@code os.name} system property and if the result does not 
1269         * start with {@literal "Windows"}, the platform is assumed to be 
1270         * {@literal "unix-like"}.
1271         * 
1272         * <p>Given the McIDAS-V supported platforms (Windows, {@literal "Unix"}, 
1273         * and OS X), the above logic is safe.
1274         * 
1275         * @return {@code true} if we're not running on Windows, {@code false} 
1276         * otherwise.
1277         * 
1278         * @throws RuntimeException if there is no property associated with 
1279         * {@code os.name}.
1280         */
1281        public static boolean isUnixLike() {
1282            String osName = System.getProperty("os.name");
1283            if (osName == null) {
1284                throw new RuntimeException("no os.name system property!");
1285            }
1286    
1287            if (System.getProperty("os.name").startsWith("Windows")) {
1288                return false;
1289            }
1290            return true;
1291        }
1292    
1293        /**
1294         * Queries the {@code os.name} system property and if the result starts 
1295         * with {@literal "Windows"}, the platform is assumed to be Windows. Duh.
1296         * 
1297         * @return {@code true} if we're running on Windows, {@code false} 
1298         * otherwise.
1299         * 
1300         * @throws RuntimeException if there is no property associated with 
1301         * {@code os.name}.
1302         */
1303        public static boolean isWindows() {
1304            String osName = System.getProperty("os.name");
1305            if (osName == null) {
1306                throw new RuntimeException("no os.name system property!");
1307            }
1308    
1309            return osName.startsWith("Windows");
1310        }
1311    
1312        /**
1313         * If McIDAS-V is running on Windows, this method will return a 
1314         * {@code String} that looks like {@literal "C:"} or {@literal "D:"}, etc.
1315         * 
1316         * <p>If McIDAS-V is not running on Windows, this method will return an
1317         * empty {@code String}.
1318         * 
1319         * @return Either the {@literal "drive letter"} of the {@code java.home} 
1320         * property or an empty {@code String} if McIDAS-V isn't running on Windows.
1321         * 
1322         * @throws RuntimeException if there is no property associated with 
1323         * {@code java.home}.
1324         */
1325        public static String getJavaDriveLetter() {
1326            if (!isWindows()) {
1327                return "";
1328            }
1329    
1330            String home = System.getProperty("java.home");
1331            if (home == null) {
1332                throw new RuntimeException("no java.home system property!");
1333            }
1334    
1335            return home.substring(0, 2);
1336        }
1337    
1338        /**
1339         * Attempts to create a {@literal "session"} file. This method will create
1340         * a {@literal "userpath"} if it does not already exist. 
1341         * 
1342         * @param path Path of the session file that should get created. 
1343         * {@code null} values are not allowed, and sufficient priviledges are 
1344         * assumed.
1345         * 
1346         * @throws AssertionError if McIDAS-V couldn't write to {@code path}.
1347         * 
1348         * @see #SESSION_FILE
1349         * @see #hadCleanExit(String)
1350         * @see #removeSessionFile(String)
1351         */
1352        private static void createSessionFile(final String path) {
1353            assert path != null : "Cannot create a null path";
1354            FileOutputStream out;
1355            PrintStream p;
1356    
1357            File dir = new File(StartupManager.INSTANCE.getPlatform().getUserDirectory());
1358            if (!dir.exists()) {
1359                dir.mkdir();
1360            }
1361    
1362            try {
1363                out = new FileOutputStream(path);
1364                p = new PrintStream(out);
1365                p.println(new Date().getTime());
1366                p.close();
1367            } catch (Exception e) {
1368                throw new AssertionError("Could not write to "+path+". Error message: "+e.getMessage());
1369            }
1370        }
1371    
1372        /**
1373         * Attempts to extract a timestamp from {@code path}. {@code path} is 
1374         * expected to <b>only</b> contain a single line consisting of a 
1375         * {@link Long} integer.
1376         * 
1377         * @param path Path to the file of interest.
1378         * 
1379         * @return Either a {@link Date} of the timestamp contained in 
1380         * {@code path} or {@code null} if the extraction failed.
1381         */
1382        private static Date extractDate(final String path) {
1383            assert path != null;
1384            Date savedDate = null;
1385            try {
1386                BufferedReader reader = new BufferedReader(new FileReader(path));
1387                String line = reader.readLine();
1388                if (line != null) {
1389                    savedDate = new Date(Long.parseLong(line.trim()));
1390                }
1391            } catch (Exception e) {
1392                logger.trace("problem extracting the date!", e);
1393            }
1394            return savedDate;
1395        }
1396    
1397        /**
1398         * Attempts to remove the file accessible via {@code path}.
1399         * 
1400         * @param path Path of the file that'll get removed. This should be 
1401         * non-null and point to an existing and writable filename (not a 
1402         * directory).
1403         * 
1404         * @throws AssertionError if the file at {@code path} could not be 
1405         * removed.
1406         * 
1407         * @see #SESSION_FILE
1408         * @see #createSessionFile(String)
1409         * @see #hadCleanExit(String)
1410         */
1411        private static void removeSessionFile(final String path) {
1412            if (path == null) {
1413                return;
1414            }
1415    
1416            File f = new File(path);
1417    
1418            if (!f.exists() || !f.canWrite() || f.isDirectory()) {
1419                return;
1420            }
1421    
1422            if (!f.delete()) {
1423                throw new AssertionError("Could not delete session file");
1424            }
1425        }
1426    
1427        /**
1428         * Tries to determine whether or not the last McIDAS-V session ended 
1429         * {@literal "cleanly"}. Currently a simple check for a 
1430         * {@literal "session"} file that is created upon starting and removed upon
1431         * ending.
1432         * 
1433         * @param path Path to the session file to check. Can't be {@code null}.
1434         * 
1435         * @return Either {@code true} if the file pointed at by {@code path} does
1436         * <b><i>NOT</i></b> exist, {@code false} if it does exist.
1437         * 
1438         * @see #SESSION_FILE
1439         * @see #createSessionFile(String)
1440         * @see #removeSessionFile(String)
1441         */
1442        private static boolean hadCleanExit(final String path) {
1443            assert path != null : "Cannot test for a null path";
1444            return !(new File(path).exists());
1445        }
1446    
1447        /**
1448         * Returns the (<i>current</i>) path to the session file. Note that the
1449         * location of the file may change arbitrarily.
1450         * 
1451         * @return {@code String} pointing to the session file.
1452         * 
1453         * @see #SESSION_FILE
1454         */
1455        public static String getSessionFilePath() {
1456            return StartupManager.INSTANCE.getPlatform().getUserFile("session.tmp");
1457        }
1458    
1459        /**
1460         * Useful for providing the startup manager with values other than the 
1461         * defaults... Note that this method attempts to update the value of 
1462         * {@link #SESSION_FILE}.
1463         * 
1464         * @param args Likely the argument array coming from the main method.
1465         */
1466        private static void applyArgs(final String[] args) {
1467            assert args != null : "Cannot use a null argument array";
1468            StartupManager.applyArgs(true, false, args);
1469            SESSION_FILE = getSessionFilePath();
1470        }
1471    
1472        /**
1473         * The main. Configure the logging and create the McIdasV
1474         * 
1475         * @param args Command line arguments
1476         * 
1477         * @throws Exception When something untoward happens
1478         */
1479        public static void main(String[] args) throws Exception {
1480    //        SysOutOverSLF4J.registerLoggingSystem("ch.qos.logback");
1481            SysOutOverSLF4J.sendSystemOutAndErrToSLF4J(LogLevel.INFO, LogLevel.WARN);
1482            logger.info("=============================================================================");
1483            logger.info("Starting McIDAS-V @ {}", new Date());
1484            logger.info("Versions:");
1485            logger.info("{}", SystemState.getMcvVersionString());
1486            logger.info("{}", SystemState.getIdvVersionString());
1487            long sysMem = Long.valueOf(SystemState.queryOpSysProps().get("opsys.memory.physical.total"));
1488            logger.info("{} MB system memory", Math.round(sysMem/1024/1024));
1489            applyArgs(args);
1490            if (!hadCleanExit(SESSION_FILE)) {
1491                previousStart = extractDate(SESSION_FILE);
1492            }
1493            createSessionFile(SESSION_FILE);
1494            LogUtil.configure();
1495            McIDASV myself = new McIDASV(args);
1496        }
1497    
1498        /**
1499         * Attempts a clean shutdown of McIDAS-V. Currently this entails 
1500         * suppressing any error dialogs, explicitly killing the 
1501         * {@link #addeEntries}, and removing {@link #SESSION_FILE}.
1502         * 
1503         * @param exitCode System exit code to use
1504         * 
1505         * @see IntegratedDataViewer#quit()
1506         */
1507        @Override protected void exit(int exitCode) {
1508            LogUtil.setShowErrorsInGui(false);
1509    
1510            if (addeEntries != null) {
1511                addeEntries.saveEntries();
1512                addeEntries.stopLocalServer();
1513            }
1514    
1515            removeSessionFile(SESSION_FILE);
1516            logger.info("Exiting McIDAS-V @ {}", new Date());
1517            System.exit(exitCode);
1518        }
1519    
1520        /**
1521         * Exposes {@link #exit(int)} to other classes.
1522         * 
1523         * @param exitCode
1524         * 
1525         * @see #exit(int)
1526         */
1527        public void exitMcIDASV(int exitCode) {
1528            exit(exitCode);
1529        }
1530    }