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