001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv;
030
031
032import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.cast;
033
034import java.io.BufferedReader;
035import java.io.BufferedWriter;
036import java.io.File;
037import java.io.FileReader;
038import java.io.FileWriter;
039
040import java.util.ArrayList;
041import java.util.Hashtable;
042import java.util.List;
043import java.util.Map;
044import java.util.Objects;
045import java.util.Properties;
046
047import java.awt.event.ActionEvent;
048
049import javax.swing.JEditorPane;
050import javax.swing.JLabel;
051import javax.swing.JOptionPane;
052import javax.swing.JPanel;
053import javax.swing.event.HyperlinkEvent;
054import javax.swing.event.HyperlinkListener;
055
056import ucar.unidata.idv.ControlDescriptor;
057import ucar.unidata.idv.IdvObjectStore;
058import ucar.unidata.idv.IntegratedDataViewer;
059import ucar.unidata.util.FileManager;
060import ucar.unidata.util.IOUtil;
061import ucar.unidata.util.LogUtil;
062import ucar.unidata.util.Misc;
063import ucar.unidata.util.StringUtil;
064
065import org.slf4j.Logger;
066import org.slf4j.LoggerFactory;
067
068import edu.wisc.ssec.mcidasv.startupmanager.StartupManager;
069import edu.wisc.ssec.mcidasv.util.SystemState;
070
071/**
072 * This class is used to initialize McIDAS-V.
073 *
074 * <p>The initialization process includes creating an
075 * {@literal "object store"} (for preferences), user-modifiable settings,
076 * and so on.</p>
077 *
078 * <p>McIDAS-V uses this class to perform pretty much all version-check
079 * operations.</p>
080 */
081public class StateManager extends ucar.unidata.idv.StateManager
082    implements Constants, HyperlinkListener
083{
084    /** Logging object. */
085    private static final Logger logger =
086        LoggerFactory.getLogger(StateManager.class);
087        
088    /** Error message shown when given userpath cannot be used. */
089    public static final String USERPATH_IS_BAD_MESSAGE =
090        "<html>McIDAS-V is unable to create or write to the local user's " +
091        "directory.<br>Please select a directory.</html>";
092        
093    /** Message shown when asking the user to select a userpath. */
094    public static final String USERPATH_PICK =
095        "Please select a directory to use as the McIDAS-V user path.";
096        
097    /**
098     * Lazily-loaded VisAD build date. Value may be {@code null}.
099     *
100     * @see #getVisadDate()
101     */
102    private String visadDate;
103    
104    /**
105     * Lazily-loaded VisAD revision number. Value may be {@code null}.
106     *
107     * @see #getVisadVersion()
108     */
109    private String visadVersion;
110    
111    /**
112     * Lazily-loaded {@code ncIdv.jar} build timestamp. Value may be
113     * {@code null}.
114     *
115     * @see #getNetcdfDate()
116     */
117    private String netcdfDate;
118    
119    /**
120     * Lazily-loaded {@code ncIdv.jar} version. Value may be {@code null}.
121     *
122     * @see #getNetcdfVersion()
123     */
124    private String netcdfVersion;
125    
126    /**
127     * Lazily-loaded {@code mcidasv.jar} version. Value may be {@code null}.
128     *
129     * @see #getMcIdasVersion()
130     */
131    private String version;
132    
133    public StateManager(IntegratedDataViewer idv) {
134        super(idv);
135    }
136    
137    /**
138     * Override to set the right user directory.
139     *
140     * @return Newly created object store.
141     */
142    @Override protected IdvObjectStore doMakeObjectStore() {
143        IdvObjectStore store = new IdvObjectStore(getIdv(),
144                                   getStoreSystemName(), getStoreName(),
145                                   getIdv().getEncoderForRead(),
146                                   StartupManager.getInstance().getPlatform().getUserDirectory());
147        initObjectStore(store);
148        return store;
149    }
150    
151    /**
152     * Initialize the given object store. This mostly initializes the user's
153     * {@literal "userpath"} directory when it is first created.
154     *
155     * @param store Object store to initialize. Cannot be {@code null}.
156     */
157    @Override protected void initObjectStore(IdvObjectStore store) {
158        while (!store.userDirectoryOk()) {
159            LogUtil.userMessage(USERPATH_IS_BAD_MESSAGE);
160            File dir = FileManager.getDirectory(null, USERPATH_PICK);
161            if (dir != null) {
162                store.setOverrideDirectory(dir);
163            } else {
164                // TODO(jon): figure out why we aren't using regular exit stuff
165                System.exit(0);
166            }
167        }
168        
169        if (store.getMadeUserDirectory()) {
170            initNewUserDirectory(store.getUserDirectory());
171        }
172        initUserDirectory(store.getUserDirectory());
173    }
174    
175    /**
176     * Initialize the McIDAS-V user directory (if it is not already
177     * initalized).
178     *
179     * <p>Here, initialization means {@literal "the user directory exists,
180     * and contains a barebones version of mcidasv.rbi"}.</p>
181     *
182     * @param directory McIDAS-V user directory. Cannot be {@code null}.
183     */
184    @Override protected void initUserDirectory(File directory) {
185        File idvRbi = new File(IOUtil.joinDir(directory, "idv.rbi"));
186        if (idvRbi.exists()) {
187            if (!idvRbi.delete()) {
188                logger.warn("Could not delete '"+idvRbi+'\'');
189            }
190        }
191        File rbiFile = new File(IOUtil.joinDir(directory, "mcidasv.rbi"));
192        if (!rbiFile.exists()) {
193            String defaultRbi =
194                IOUtil.readContents(
195                    "/edu/wisc/ssec/mcidasv/resources/userrbi.rbi",
196                    (String)null);
197            if (defaultRbi != null) {
198                try {
199                    IOUtil.writeFile(rbiFile, defaultRbi);
200                } catch (Exception exc) {
201                    logException("Writing default rbi", exc);
202                }
203            }
204        }
205    }
206    
207    @Override protected void initState(boolean interactiveMode) {
208        super.initState(interactiveMode);
209        
210        // At this point in the startup process, McV has read and initialized
211        // the resources specified in mcidasv.rbi. filtering out the disabled
212        // controls here allows us to do the work once and have it apply to
213        // the rest of the session.
214        removeDisabledControlDescriptors();
215    }
216    
217    /**
218     * Removes disabled {@link ControlDescriptor ControlDescriptors} from
219     * {@link IntegratedDataViewer IntegratedDataViewer's}
220     * {@code controlDescriptors} and {@code controlDescriptorMap} fields.
221     */
222    private void removeDisabledControlDescriptors() {
223        McIDASV mcv = (McIDASV)getIdv();
224        Map<String, ControlDescriptor> cdMap = mcv.getControlDescriptorMap();
225        List<ControlDescriptor> cds = cast(mcv.getAllControlDescriptors());
226        // copy is soley for iterating over (avoid concurrent modification probs)
227        List<ControlDescriptor> copy = new ArrayList<>(cds);
228        for (ControlDescriptor c : copy) {
229            Hashtable<String, String> props = cast(c.getProperties());
230            String v = props.getOrDefault("disabled", "false");
231            if (Objects.equals(v, "true")) {
232                cdMap.remove(c.getControlId());
233                cds.remove(c);
234            }
235        }
236    }
237    
238    /**
239     * Handle a change to a link.
240     *
241     * @param e Link event. Cannot be {@code null}.
242     */
243    @Override public void hyperlinkUpdate(HyperlinkEvent e) {
244        if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
245            if (e.getURL() == null) {
246                click(e.getDescription());
247            } else {
248                click(e.getURL().toString());
249            }
250        }
251    }
252    
253    /**
254     * Handle a click on a link.
255     *
256     * @param url Link to visit.
257     */
258    public void click(String url) {
259        getIdv().actionPerformed(new ActionEvent(this, 0, url));
260    }
261    
262    /**
263     * Get the name of the current operating system (via
264     * {@literal "os.name"} system property).
265     *
266     * <p>Note: all space characters will be replaced with underscores.</p>
267     *
268     * @return Operating system name.
269     */
270    public String getOSName() {
271        String os = System.getProperty("os.name");
272        os = os.replaceAll(" ", "_");
273        return os;
274    }
275    
276    public String getMcIdasVersionAbout() {
277        if (version == null) {
278            getMcIdasVersion();
279        }
280        
281        String aboutText = (String)getProperty(Constants.PROP_ABOUTTEXT);
282        String versionAbout = IOUtil.readContents(aboutText, "");
283        versionAbout = StringUtil.replace(versionAbout, MACRO_VERSION, version);
284        Properties props = Misc.readProperties(
285            (String) getProperty(Constants.PROP_VERSIONFILE), 
286            null, 
287            getClass()
288        );
289        
290        String value = getIdvVersion();
291        versionAbout = StringUtil.replace(versionAbout, Constants.MACRO_IDV_VERSION, value);
292        value = props.getProperty(PROP_COPYRIGHT_YEAR, "");
293        versionAbout = StringUtil.replace(versionAbout, Constants.MACRO_COPYRIGHT_YEAR, value);
294        value = props.getProperty(PROP_BUILD_DATE, "Unknown");
295        versionAbout = StringUtil.replace(versionAbout, Constants.MACRO_BUILDDATE, value);
296        versionAbout = StringUtil.replace(versionAbout, Constants.MACRO_VISAD_VERSION, getVisadVersion());
297        
298        return versionAbout;
299    }
300    
301    public String getMcIdasVersion() {
302        if (version != null) {
303            return version;
304        }
305        
306        Properties props = new Properties();
307        props = Misc.readProperties((String) getProperty(Constants.PROP_VERSIONFILE), null, getClass());
308        String maj = props.getProperty(PROP_VERSION_MAJOR, "0");
309        String min = props.getProperty(PROP_VERSION_MINOR, "0");
310        String rel = props.getProperty(PROP_VERSION_RELEASE, "");
311        
312        version = maj.concat(".").concat(min).concat(rel);
313        
314        return version;
315    }
316    
317    /**
318     * Returns the current Jython version.
319     * 
320     * @return Jython's version information.
321     */
322    @Override public String getJythonVersion() {
323        return org.python.Version.PY_VERSION;
324    }
325    
326    /**
327     * Get a property.
328     *
329     * @param name Name of the property. Cannot be {@code null}.
330     *
331     * @return Value associated with {@code name} or {@code null}.
332     */
333    @Override public Object getProperty(final String name) {
334        Object value = null;
335        if (McIDASV.isMac()) {
336            value = getProperties().get("mac."+name);
337        }
338        if (value == null) {
339            value = getProperties().get(name);
340        }
341        if (value == null) {
342            String fixedName = StateManager.fixIds(name);
343            if (!name.equals(fixedName)) {
344                return getProperties().get(fixedName);
345            }
346        }
347        return value;
348    }
349    
350    /**
351     * Find the value associated with the given ID by checking the
352     * {@literal "properties"}, and if nothing was found, check the preferences.
353     *
354     * @param name Property or preference ID. Cannot be {@code null}.
355     *
356     * @return Either the value associated with {@code name} or {@code null}.
357     */
358    public Object getPropertyOrPreference(String name) {
359        Object o = getProperty(name);
360        if (o == null) {
361            o = getPreference(name);
362        }
363        return o;
364    }
365    
366    /**
367     * Find the {@link String} value associated with the given ID by checking
368     * the {@literal "properties"}, and if nothing was found, check the
369     * preferences.
370     *
371     * @param name Property or preference ID. Cannot be {@code null}.
372     * @param dflt Value to return if there is no property or preference
373     * associated with {@code name}
374     *
375     * @return Either the value associated with {@code name} or {@code dflt}.
376     */
377    public String getPropertyOrPreference(String name, String dflt) {
378        String value = dflt;
379        Object o = getPropertyOrPreference(name);
380        if (o != null) {
381            value = o.toString();
382        }
383        return value;
384    }
385    
386    /**
387     * Find the {@link Integer} value associated with the given ID by checking
388     * the {@literal "properties"}, and if nothing was found, check the
389     * preferences.
390     *
391     * @param name Property or preference ID. Cannot be {@code null}.
392     * @param dflt Value to return if there is no property or preference
393     * associated with {@code name}
394     *
395     * @return Either the value associated with {@code name} or {@code dflt}.
396     */
397    public int getPropertyOrPreference(String name, int dflt) {
398        int value = dflt;
399        Object o = getPropertyOrPreference(name);
400        if (o != null) {
401            value = Integer.valueOf(o.toString());
402        }
403        return value;
404    }
405    
406    /**
407     * Find the {@link Double} value associated with the given ID by checking
408     * the {@literal "properties"}, and if nothing was found, check the
409     * preferences.
410     *
411     * @param name Property or preference ID. Cannot be {@code null}.
412     * @param dflt Value to return if there is no property or preference
413     * associated with {@code name}
414     *
415     * @return Either the value associated with {@code name} or {@code dflt}.
416     */
417    public double getPropertyOrPreference(String name, double dflt) {
418        double value = dflt;
419        Object o = getPropertyOrPreference(name);
420        if (o != null) {
421            value = Double.valueOf(o.toString());
422        }
423        return value;
424    }
425    
426    /**
427     * Find the {@link Boolean} value associated with the given ID by checking
428     * the {@literal "properties"}, and if nothing was found, check the
429     * preferences.
430     *
431     * @param name Property or preference ID. Cannot be {@code null}.
432     * @param dflt Value to return if there is no property or preference
433     * associated with {@code name}
434     *
435     * @return Either the value associated with {@code name} or {@code dflt}.
436     */
437    public boolean getPropertyOrPreference(String name, boolean dflt) {
438        boolean value = dflt;
439        Object o = getPropertyOrPreference(name);
440        if (o != null) {
441            value = Boolean.valueOf(o.toString());
442        }
443        return value;
444    }
445    
446    /**
447     * Returns information about the current version of McIDAS-V and the IDV,
448     * along with their respective build dates.
449     * 
450     * @return {@code Hashtable} containing versioning information.
451     */
452    public Hashtable<String, String> getVersionInfo() {
453        String versionFile = (String)getProperty(Constants.PROP_VERSIONFILE);
454        Properties props =
455            Misc.readProperties(versionFile, null, getClass());
456            
457        String mcvBuild = props.getProperty(PROP_BUILD_DATE, "Unknown");
458        
459        Hashtable<String, String> table = new Hashtable<>();
460        table.put("mcv.version.general", getMcIdasVersion());
461        table.put("mcv.version.build", mcvBuild);
462        table.put("idv.version.general", getVersion());
463        table.put("idv.version.build", getBuildDate());
464        table.put("visad.version.general", getVisadVersion());
465        table.put("visad.version.build", getVisadDate());
466        table.put("netcdf.version.general", getNetcdfVersion());
467        table.put("netcdf.version.build", getNetcdfDate());
468        return table;
469    }
470    
471    /**
472     * Return the timestamp from when {@code ncIdv.jar} was created.
473     *
474     * @return {@code String} representation of the creation timestamp.
475     */
476    public String getNetcdfDate() {
477        if (netcdfDate == null) {
478            Map<String, String> props = SystemState.queryNcidvBuildProperties();
479            netcdfDate = props.get("buildDate");
480            netcdfVersion = props.get("version");
481        }
482        return netcdfDate;
483    }
484    
485    /**
486     * Return the version information within {@code ncIdv.jar}.
487     *
488     * @return Version of {@code ncIdv.jar} shipped by McIDAS-V.
489     */
490    public String getNetcdfVersion() {
491        if (netcdfVersion == null) {
492            Map<String, String> props = SystemState.queryNcidvBuildProperties();
493            netcdfDate = props.get("buildDate");
494            netcdfVersion = props.get("version");
495        }
496        return netcdfVersion;
497    }
498    
499    /**
500     * Return the timestamp from when visad.jar was created.
501     * 
502     * @return {@code String} representation of the creation timestamp.
503     * Likely to change formatting over time.
504     */
505    public String getVisadDate() {
506        if (visadDate == null) {
507            Map<String, String> props = SystemState.queryVisadBuildProperties();
508            visadDate = props.get(Constants.PROP_VISAD_DATE);
509            visadVersion = props.get(Constants.PROP_VISAD_REVISION);
510        }
511        return visadDate;
512    }
513    
514    /**
515     * Return the {@literal "version"} of VisAD.
516     * 
517     * @return Currently returns whatever the SVN revision number was when
518     * visad.jar was built. 
519     */
520    public String getVisadVersion() {
521        if (visadVersion == null) {
522            Map<String, String> props = SystemState.queryVisadBuildProperties();
523            visadDate = props.get(Constants.PROP_VISAD_DATE);
524            visadVersion = props.get(Constants.PROP_VISAD_REVISION);
525        }
526        return visadVersion;
527    }
528    
529    public String getIdvVersion() {
530        return getVersion();
531    }
532    
533    /**
534     * Overridden to set default of McIDAS-V
535     */
536    @Override public String getStoreSystemName() {
537        return StartupManager.getInstance().getPlatform().getUserDirectory();
538    }
539    
540    /**
541     * Overridden to get dir of the unnecessary second level directory.
542     */
543    @Override public String getStoreName() {
544        return "";
545    }
546    
547    /**
548     * Connect to McIDAS-V website and look for latest stable version.
549     *
550     * @return Latest stable version.
551     */
552    public String getMcIdasVersionStable() {
553        String offscreen = "0";
554        if (getIdv().getArgsManager().getIsOffScreen()) {
555            offscreen = "1";
556        }
557
558        String version = "";
559        try {
560            version = IOUtil.readContents(Constants.HOMEPAGE_URL+"/"+Constants.VERSION_HANDLER_URL+"?v="+getMcIdasVersion()+"&os="+getOSName()+"&off="+offscreen, "");
561        } catch (Exception e) {
562            logger.warn("Could not get latest McV stable version", e);
563        }
564        return version.trim();
565    }
566    
567    /**
568     * Connect to McIDAS-V website and look for latest pre-release version.
569     *
570     * @return Latest pre-release version.
571     */
572    public String getMcIdasVersionPrerelease() {
573        String version = "";
574        try {
575            String htmlList = IOUtil.readContents(Constants.HOMEPAGE_URL+'/'+Constants.PRERELEASE_URL, "");
576            String lines[] = htmlList.split("\n");
577            for (int i=0; i<lines.length; i++) {
578                String line = lines[i].trim();
579                if (line.matches(".*McIDAS-V_\\d+\\.\\d+.*")) {
580                    line = line.substring(line.indexOf("McIDAS-V_")+9);
581                    String aVersion = line.substring(0, line.indexOf("_"));
582                    if (version.isEmpty()) {
583                        version = aVersion;
584                    } else {
585                        int comp = compareVersions(version, aVersion);
586                        if (comp > 0) {
587                            version = aVersion;
588                        }
589                    }
590                }
591            }
592        } catch (Exception e) {
593            logger.warn("Could not get latest McV pre-release version", e);
594        }
595        return version.trim();
596    }
597    
598    /**
599     * Connect to McIDAS website and look for latest notice.
600     *
601     * @return Contents of notice. String may be empty.
602     */
603    public String getNoticeLatest() {
604        String notice = "";
605        try {
606            notice = IOUtil.readContents(Constants.HOMEPAGE_URL+"/"+Constants.NOTICE_URL+"?requesting="+getMcIdasVersion()+"&os="+getOSName(), "");
607        } catch (Exception e) {
608            logger.warn("Could not get latest notice", e);
609        }
610        if (!notice.contains("<notice>")) {
611            notice = "";
612        }
613        notice = notice.replaceAll("<[/?]notice>","");
614        return notice.trim();
615    }
616    
617    /**
618     * Compare version strings.
619     *
620     * <p>The logic is as follows.
621     * <pre>
622     *    0: thisVersion and thatVersion are equal.
623     *   &lt;0: thisVersion is greater.
624     *   &gt;0: thatVersion is greater.
625     * </pre>
626     *
627     * @param thisVersion First version string to compare.
628     * @param thatVersion Second version string to compare.
629     *
630     * @return Value indicating which of {@code thisVersion} and
631     * {@code thatVersion} is {@literal "greater"}.
632     */
633    public static int compareVersions(String thisVersion, String thatVersion) {
634        int thisInt = versionToInteger(thisVersion);
635        int thatInt = versionToInteger(thatVersion);
636        return thatInt - thisInt;
637    }
638    
639    /**
640     * Turn version strings of the form {@code #.#(a#)}, where # is one or two
641     * digits, a is one of alpha or beta, and () is optional, into an integer
642     * value... (empty) &gt; beta &gt; alpha.
643     *
644     * @param version String representation of version number.
645     *
646     * @return Integer representation of {@code version}.
647     */
648    public static int versionToInteger(String version) {
649        int value = 0;
650        int p;
651        String part;
652        Character one = null;
653        
654        try {
655            // Major version
656            p = version.indexOf('.');
657            if (p > 0) {
658                part = version.substring(0,p);
659                value += Integer.parseInt(part) * 1000000;
660                version = version.substring(p+1);
661            }
662            
663            // Minor version
664            int minor = 0;
665            int i = 0;
666            for (i = 0; i < 2 && i < version.length(); i++) {
667                one = version.charAt(i);
668                if (Character.isDigit(one)) {
669                    if (i > 0) {
670                        minor *= 10;
671                    }
672                    minor += Character.digit(one, 10) * 10000;
673                } else {
674                    break;
675                }
676            }
677            value += minor;
678            if (one != null) {
679                version = version.substring(i);
680            }
681            
682            // Alpha/beta/update/release status
683            if (version.length() == 0) {
684                value += 300;
685            } else if (version.charAt(0) == 'b') {
686                value += 200;
687            } else if (version.charAt(0) == 'a') {
688                value += 100;
689            } else if (version.charAt(0) == 'u') {
690                value += 400;
691            } else if (version.charAt(0) == 'r') {
692                value += 400;
693            }
694            for (i = 0; i < version.length(); i++) {
695                one = version.charAt(i);
696                if (Character.isDigit(one)) {
697                    break;
698                }
699            }
700            if (one != null) {
701                version = version.substring(i);
702            }
703            
704            // Alpha/beta version
705            if (version.length() > 0) {
706                value += Integer.parseInt(version);
707            }
708        } catch (Exception e) {
709            
710        }
711        return value;
712    }
713    
714    public boolean getIsPrerelease() {
715        boolean isPrerelease = false;
716        String version = getMcIdasVersion();
717        if (version.contains("a") || version.contains("b")) {
718            isPrerelease = true;
719        }
720        return isPrerelease;
721    }
722    
723    public void checkForNewerVersion(boolean notifyDialog) {
724        checkForNewerVersionStable(notifyDialog);
725        if (getStore().get(Constants.PREF_PRERELEASE_CHECK, getIsPrerelease())) {
726            checkForNewerVersionPrerelease(notifyDialog);
727        }
728    }
729    
730    public void checkForNewerVersionStable(boolean notifyDialog) {
731        
732        // get the stable version from the website (for statistics recording)
733        String thatVersion = getMcIdasVersionStable();
734        
735        // shortcut the rest of the process if we are processing offscreen
736        if (getIdv().getArgsManager().getIsOffScreen()) {
737            return;
738        }
739        
740        String thisVersion = getMcIdasVersion();
741        String titleText = "Version Check";
742        
743        if (thisVersion.isEmpty() || thatVersion.isEmpty()) {
744            if (notifyDialog) {
745                JOptionPane.showMessageDialog(null,
746                                              "Version check failed",
747                                              titleText,
748                                              JOptionPane.WARNING_MESSAGE);
749            }
750        } else if (compareVersions(thisVersion, thatVersion) > 0) {
751            String labelText = "<html>Version <b>" + thatVersion + "</b> is available<br><br>";
752            labelText += "Visit <a href=\"" + Constants.HOMEPAGE_URL + "\">";
753            labelText += Constants.HOMEPAGE_URL + "</a> to download</html>";
754            
755            JPanel backgroundColorGetterPanel = new JPanel();
756            JEditorPane messageText = new JEditorPane("text/html", labelText);
757            messageText.setBackground(backgroundColorGetterPanel.getBackground());
758            messageText.setEditable(false);
759            messageText.addHyperlinkListener(this);
760            
761            //JLabel message = new JLabel(labelText, JLabel.CENTER);
762            JOptionPane.showMessageDialog(null,
763                                          messageText,
764                                          titleText,
765                                          JOptionPane.INFORMATION_MESSAGE);
766        } else {
767            if (notifyDialog) {
768                String labelText = "<html>This version (<b>" + thisVersion + "</b>) is up to date</html>";
769                JLabel message = new JLabel(labelText, JLabel.CENTER);
770                JOptionPane.showMessageDialog(null,
771                                              message,
772                                              titleText,
773                                              JOptionPane.INFORMATION_MESSAGE);
774            }
775        }
776    }
777    
778    public void checkForNewerVersionPrerelease(boolean notifyDialog) {
779        
780        // shortcut the rest of the process if we are processing offscreen
781        if (getIdv().getArgsManager().getIsOffScreen()) {
782            return;
783        }
784        
785        String thisVersion = getMcIdasVersion();
786        String thatVersion = getMcIdasVersionPrerelease();
787        String titleText = "Prerelease Check";
788        
789        if (thisVersion.isEmpty() || thatVersion.isEmpty()) {
790            if (notifyDialog) {
791                JOptionPane.showMessageDialog(
792                    null, "No prerelease version available", titleText,
793                    JOptionPane.WARNING_MESSAGE);
794            }
795        } else if (compareVersions(thisVersion, thatVersion) > 0) {
796            String labelText = "<html>Prerelease <b>" + thatVersion + "</b> is available<br><br>";
797            labelText += "Visit <a href=\"" + Constants.HOMEPAGE_URL+'/'+Constants.PRERELEASE_URL + "\">";
798            labelText += Constants.HOMEPAGE_URL+'/'+Constants.PRERELEASE_URL + "</a> to download</html>";
799            
800            JPanel backgroundColorGetterPanel = new JPanel();
801            JEditorPane messageText = new JEditorPane("text/html", labelText);
802            messageText.setBackground(backgroundColorGetterPanel.getBackground());
803            messageText.setEditable(false);
804            messageText.addHyperlinkListener(this);
805            
806            // JLabel message = new JLabel(labelText, JLabel.CENTER);
807            JOptionPane.showMessageDialog(null,
808                                          messageText,
809                                          titleText,
810                                          JOptionPane.INFORMATION_MESSAGE);
811        } else {
812            if (notifyDialog) {
813                String labelText = "<html>This version (<b>" + thisVersion + "</b>) is up to date</html>";
814                JLabel message = new JLabel(labelText, JLabel.CENTER);
815                JOptionPane.showMessageDialog(null,
816                                              message,
817                                              titleText,
818                                              JOptionPane.INFORMATION_MESSAGE);
819            }
820        }
821    }
822    
823    public void checkForNotice(boolean notifyDialog) {
824        
825        // Shortcut this whole process if we are processing offscreen
826        if (getIdv().getArgsManager().getIsOffScreen()) {
827            return;
828        }
829        
830        String thisNotice = getNoticeCached().trim();
831        String thatNotice = getNoticeLatest().trim();
832        String titleText = "New Notice";
833        String labelText = thatNotice;
834        
835        if (thatNotice.isEmpty()) {
836            setNoticeCached(thatNotice);
837            if (notifyDialog) {
838                titleText = "No Notice";
839                JLabel message = new JLabel("There is no current notice", JLabel.CENTER);
840                JOptionPane.showMessageDialog(null, message, titleText,
841                    JOptionPane.INFORMATION_MESSAGE);
842            }
843        } else if (!thisNotice.equals(thatNotice)) {
844            setNoticeCached(thatNotice);
845            
846            JPanel backgroundColorGetterPanel = new JPanel();
847            JEditorPane messageText = new JEditorPane("text/html", labelText);
848            messageText.setBackground(backgroundColorGetterPanel.getBackground());
849            messageText.setEditable(false);
850            messageText.addHyperlinkListener(this);
851            JOptionPane.showMessageDialog(null, messageText, titleText,
852                JOptionPane.INFORMATION_MESSAGE);
853        } else {
854            if (notifyDialog) {
855                titleText = "Previous Notice";
856                JPanel bgPanel = new JPanel();
857                JEditorPane messageText =
858                    new JEditorPane("text/html", labelText);
859                messageText.setBackground(bgPanel.getBackground());
860                messageText.setEditable(false);
861                messageText.addHyperlinkListener(this);
862                JOptionPane.showMessageDialog(null, messageText, titleText,
863                    JOptionPane.INFORMATION_MESSAGE);
864            }
865        }
866    }
867    
868    /**
869     * Debug a McIDAS-V {@literal "system notice"} before sending it to all
870     * users!
871     *
872     * @param noticeContents Contents of the notice.
873     * @param notifyDialog if {@code true}, show notice even if already seen.
874     * @param disableCache Whether or not {@code noticeContents} will be cached.
875     */
876    public void debugNotice(String noticeContents, boolean notifyDialog,
877                            boolean disableCache)
878    {
879        // Shortcut this whole process if we are processing offscreen
880        if (getIdv().getArgsManager().getIsOffScreen()) {
881            return;
882        }
883        
884        String thisNotice;
885        thisNotice = disableCache ? "" : getNoticeCached().trim();
886        String thatNotice = noticeContents.trim();
887        String labelText = thatNotice;
888        
889        if (thatNotice.isEmpty()) {
890            if (!disableCache) {
891                setNoticeCached(thatNotice);
892            }
893            if (notifyDialog) {
894                String titleText = "No Notice";
895                JLabel message =
896                    new JLabel("There is no current notice", JLabel.CENTER);
897                JOptionPane.showMessageDialog(null, message, titleText,
898                    JOptionPane.INFORMATION_MESSAGE);
899            }
900        } else if (!thisNotice.equals(thatNotice)) {
901            if (!disableCache) {
902                setNoticeCached(thatNotice);
903            }
904            String titleText = "New Notice";
905            JPanel backgroundColorGetterPanel = new JPanel();
906            JEditorPane messageText = new JEditorPane("text/html", labelText);
907            messageText.setBackground(backgroundColorGetterPanel.getBackground());
908            messageText.setEditable(false);
909            messageText.addHyperlinkListener(this);
910            JOptionPane.showMessageDialog(null, messageText, titleText,
911                JOptionPane.INFORMATION_MESSAGE);
912        } else {
913            if (notifyDialog) {
914                String titleText = "Previous Notice";
915                JPanel bgPanel = new JPanel();
916                JEditorPane messageText =
917                    new JEditorPane("text/html", labelText);
918                messageText.setBackground(bgPanel.getBackground());
919                messageText.setEditable(false);
920                messageText.addHyperlinkListener(this);
921                JOptionPane.showMessageDialog(null, messageText, titleText,
922                    JOptionPane.INFORMATION_MESSAGE);
923            }
924        }
925    }
926    
927    private String getNoticePath() {
928        return StartupManager.getInstance().getPlatform().getUserFile("notice.txt");
929    }
930    
931    private String getNoticeCached() {
932        StringBuilder notice = new StringBuilder(1024);
933        try{
934            FileReader fstream = new FileReader(getNoticePath());
935            BufferedReader in = new BufferedReader(fstream);
936            String line;
937            while ((line = in.readLine()) != null) {
938                notice.append(line).append('\n');
939            }
940            in.close();
941        } catch (Exception e) {
942            logger.warn("Could not get cached notice", e);
943        }
944        return notice.toString();
945    }
946    
947    private void setNoticeCached(String notice) {
948        try {
949            FileWriter fstream = new FileWriter(getNoticePath());
950            BufferedWriter out = new BufferedWriter(fstream);
951            out.write(notice);
952            out.close();
953        } catch (Exception e) {
954            logger.warn("Could not cache downloaded notice", e);
955        }
956    }
957}