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.chooser.adde;
030
031import static edu.wisc.ssec.mcidasv.McIDASV.isLoopback;
032import static edu.wisc.ssec.mcidasv.servermanager.AddeEntry.DEFAULT_ACCOUNT;
033import static edu.wisc.ssec.mcidasv.servermanager.EntryTransforms.strToEntryType;
034import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
035import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.cast;
036
037import static javax.swing.GroupLayout.DEFAULT_SIZE;
038import static javax.swing.GroupLayout.Alignment.BASELINE;
039import static javax.swing.GroupLayout.Alignment.LEADING;
040import static javax.swing.GroupLayout.Alignment.TRAILING;
041import static javax.swing.LayoutStyle.ComponentPlacement.RELATED;
042import static javax.swing.LayoutStyle.ComponentPlacement.UNRELATED;
043
044import java.awt.Component;
045import java.awt.Dimension;
046import java.awt.event.ActionEvent;
047import java.awt.event.ActionListener;
048import java.awt.event.ItemEvent;
049import java.awt.event.ItemListener;
050import java.awt.event.KeyEvent;
051import java.awt.event.KeyListener;
052import java.awt.event.MouseAdapter;
053import java.awt.event.MouseEvent;
054import java.io.EOFException;
055import java.io.InputStream;
056import java.io.PrintWriter;
057import java.io.StringWriter;
058import java.io.Writer;
059import java.net.ConnectException;
060import java.net.URLConnection;
061import java.util.ArrayList;
062import java.util.Arrays;
063import java.util.Collections;
064import java.util.Comparator;
065import java.util.Enumeration;
066import java.util.HashMap;
067import java.util.Hashtable;
068import java.util.LinkedHashMap;
069import java.util.List;
070import java.util.Map;
071import java.util.Objects;
072import java.util.Vector;
073import java.util.regex.Pattern;
074
075import javax.swing.GroupLayout;
076import javax.swing.JButton;
077import javax.swing.JCheckBox;
078import javax.swing.JComboBox;
079import javax.swing.JComponent;
080import javax.swing.JLabel;
081import javax.swing.JMenu;
082import javax.swing.JMenuItem;
083import javax.swing.JPanel;
084import javax.swing.JPopupMenu;
085import javax.swing.JScrollPane;
086import javax.swing.JTabbedPane;
087import javax.swing.JTextField;
088import javax.swing.SwingUtilities;
089import javax.swing.event.DocumentEvent;
090import javax.swing.event.DocumentListener;
091import javax.swing.text.BadLocationException;
092
093import org.bushe.swing.event.annotation.AnnotationProcessor;
094import org.bushe.swing.event.annotation.EventSubscriber;
095import org.slf4j.Logger;
096import org.slf4j.LoggerFactory;
097import org.w3c.dom.Element;
098
099import ucar.unidata.idv.chooser.IdvChooser;
100import ucar.unidata.idv.chooser.IdvChooserManager;
101import ucar.unidata.idv.chooser.adde.AddeServer;
102import ucar.unidata.idv.chooser.adde.AddeServer.Group;
103import ucar.unidata.util.DatedThing;
104import ucar.unidata.util.GuiUtils;
105import ucar.unidata.util.IOUtil;
106import ucar.unidata.util.LogUtil;
107import ucar.unidata.util.Misc;
108import ucar.unidata.util.PreferenceList;
109import ucar.unidata.util.StringUtil;
110import ucar.unidata.xml.XmlObjectStore;
111
112import visad.DateTime;
113
114import edu.wisc.ssec.mcidas.adde.AddeURLException;
115import edu.wisc.ssec.mcidas.adde.DataSetInfo;
116import edu.wisc.ssec.mcidasv.Constants;
117import edu.wisc.ssec.mcidasv.McIDASV;
118import edu.wisc.ssec.mcidasv.ParameterSet;
119import edu.wisc.ssec.mcidasv.PersistenceManager;
120import edu.wisc.ssec.mcidasv.servermanager.AddeAccount;
121import edu.wisc.ssec.mcidasv.servermanager.AddeEntry;
122import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EditorAction;
123import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource;
124import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
125import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity;
126import edu.wisc.ssec.mcidasv.servermanager.EntryStore;
127import edu.wisc.ssec.mcidasv.servermanager.EntryTransforms;
128import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry;
129import edu.wisc.ssec.mcidasv.servermanager.LocalEntryEditor;
130import edu.wisc.ssec.mcidasv.servermanager.RemoteAddeEntry;
131import edu.wisc.ssec.mcidasv.servermanager.RemoteEntryEditor;
132import edu.wisc.ssec.mcidasv.servermanager.TabbedAddeManager;
133import edu.wisc.ssec.mcidasv.ui.MenuScroller;
134import edu.wisc.ssec.mcidasv.ui.ParameterTree;
135import edu.wisc.ssec.mcidasv.ui.UIManager;
136import edu.wisc.ssec.mcidasv.util.CollectionHelpers;
137import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
138import edu.wisc.ssec.mcidasv.util.McVGuiUtils.Position;
139import edu.wisc.ssec.mcidasv.util.McVGuiUtils.TextColor;
140import edu.wisc.ssec.mcidasv.util.McVGuiUtils.Width;
141import edu.wisc.ssec.mcidasv.util.McVTextField;
142
143/**
144 *
145 * @version $Revision$
146 */
147
148public class AddeChooser extends ucar.unidata.idv.chooser.adde.AddeChooser implements Constants {
149    
150    private static final long serialVersionUID = 1L;
151
152    private static final Logger logger = LoggerFactory.getLogger(AddeChooser.class);
153    
154    /** Label to use with the relative times {@link JTextField}. */
155    public static final String RELATIVE_TIMES_LABEL = "Number of times: ";
156    
157    /** Tooltip for the relative times {@link JTextField}. */
158    public static final String RELATIVE_TIMES_TOOLTIP =
159        "<html>Load the N most recent images.<br/><br/>" +
160        "Values must be integers greater than zero.</html>";
161        
162    private JComboBox serverSelector;
163    
164    /** List of descriptors */
165    private PreferenceList descList;
166    
167    /** Descriptor/name hashtable */
168    protected Hashtable descriptorTable;
169    
170    /** List of available descriptors. */
171    protected List<String> descriptorList;
172    
173    /** List of comments associated with list of descriptors. */
174    protected List<String> commentList;
175    
176    /** Property for the descriptor table */
177    public static final String DESCRIPTOR_TABLE = "DESCRIPTOR_TABLE";
178
179    /** Archive day selector button. */
180    protected JButton archiveDayBtn;
181
182    /** archive date */
183    protected String archiveDay = null;
184
185    /** archive date */
186    protected String archiveBegTime = null;
187
188    /** archive date */
189    protected String archiveEndTime = null;
190
191    /** Button label for day and optional time range selection */
192    protected final static String DAY_TIME_RANGE_LABEL = "Set Day/Time Range";
193
194    /** Connect button--we need to be able to disable this */
195    JButton connectButton = McVGuiUtils.makeImageTextButton(ICON_CONNECT_SMALL, "Connect");
196
197    /** Parameter button--we need to be able to disable this */
198    JButton parameterButton =
199        McVGuiUtils.makeImageButton("/edu/wisc/ssec/mcidasv/resources/icons/toolbar/document-open22.png",
200            this, "doParameters", null, "Load parameter set");
201
202    /** Manage button */
203    JButton manageButton =
204        McVGuiUtils.makeImageButton("/edu/wisc/ssec/mcidasv/resources/icons/toolbar/preferences-system22.png",
205            this, "doManager", null, "Manage servers");
206
207    /** Public button--we need to draw a menu from this */
208    protected JButton publicButton =
209        McVGuiUtils.makeImageButton("/edu/wisc/ssec/mcidasv/resources/icons/toolbar/show-layer-controls22.png",
210            this, "showGroups", null, "List public datasets");
211
212    /** descriptor label */
213    protected JLabel descriptorLabel = new JLabel(getDescriptorLabel()+":");
214
215    /** A widget for the list of dataset descriptors */
216    protected JComboBox descriptorComboBox = new JComboBox();
217
218    /** The descriptor names */
219    protected String[] descriptorNames;
220
221    /** Flag to keep from infinite looping */
222    protected boolean ignoreDescriptorChange = false;
223
224    /**
225     * List of JComponent-s that depend on a descriptor being selected
226     * to be enabled
227     */
228    protected ArrayList compsThatNeedDescriptor = new ArrayList();
229
230    /** Selection label text */
231    protected String LABEL_SELECT = " -- Select -- ";
232
233    /** Separator string */
234    protected static String separator = "----------------";
235
236    /** Name separator string */
237    protected static String nameSeparator = " - ";
238
239    /** Reference back to the server manager */
240    protected EntryStore serverManager;
241
242    public boolean allServersFlag;
243
244    /** Command for opening up the server manager */
245    protected static final String CMD_MANAGER = "cmd.manager";
246
247    private String lastBadServer = "";
248    private String lastBadGroup = "";
249
250    private String lastServerName = "";
251    private String lastServerGroup = "";
252    private String lastServerUser = "";
253    private String lastServerProj = "";
254    private AddeServer lastServer = new AddeServer("");
255
256    private List<AddeServer> addeServers;
257
258    /** Used for parameter set restore */
259    private static final String ATTR_SERVER = "server";
260    private static final String ATTR_GROUP = "GROUP";
261    private static final String ATTR_DESCRIPTOR = "DESCRIPTOR";
262    private static final String ATTR_POS = "POS";
263    private static final String ATTR_DAY = "DAY";
264    private static final String ATTR_TIME = "TIME";
265    private List restoreTimes = new ArrayList();
266    public Element restoreElement;
267    private boolean shouldAddSource = false;
268    final JCheckBox cb = new JCheckBox("Add source",shouldAddSource);
269
270    /** Maps favorite type to the BundleTree that shows the Manage window for the type */
271    private Hashtable parameterTrees = new Hashtable();
272    
273    /** Number of relative time steps to load */
274    private int relativeTimes = 5;
275    
276    /**
277     * Create an AddeChooser associated with an IdvChooser
278     *
279     * @param mgr The chooser manager
280     * @param root The chooser.xml node
281     */
282    public AddeChooser(IdvChooserManager mgr, Element root) {
283        super(mgr, root);
284        AnnotationProcessor.process(this);
285        descriptorList = new ArrayList<String>();
286        commentList = new ArrayList<String>();
287        
288        simpleMode = !getProperty(IdvChooser.ATTR_SHOWDETAILS, true);
289
290        loadButton = McVGuiUtils.makeImageTextButton(ICON_ACCEPT_SMALL, getLoadCommandName());
291        loadButton.setActionCommand(getLoadCommandName());
292        loadButton.addActionListener(this);
293
294        cancelButton = McVGuiUtils.makeImageButton(ICON_CANCEL, "Cancel");
295        cancelButton.setActionCommand(GuiUtils.CMD_CANCEL);
296        cancelButton.addActionListener(this);
297        cancelButton.setEnabled(false);
298
299        serverSelector = getServerSelector();
300
301        serverSelector.setToolTipText("Right click to manage servers");
302        serverSelector.getEditor().getEditorComponent().addMouseListener(
303            new MouseAdapter() {
304                public void mouseReleased(MouseEvent e) {
305                    if (!SwingUtilities.isRightMouseButton(e)) {
306                        return;
307                    }
308
309                    AddeServer server = getAddeServer();
310                    if (server == null) {
311                        return;
312                    }
313                    List<JMenuItem> items = new ArrayList<JMenuItem>();
314
315                    // Set the right-click behavior
316                    if (isLocalServer()) {
317                        items.add(GuiUtils.makeMenuItem("Manage local ADDE data",
318                            AddeChooser.this,
319                            "doManager", null));
320                    }
321                    else {
322                        items.add(GuiUtils.makeMenuItem("Manage ADDE servers",
323                            AddeChooser.this,
324                            "doManager", null));
325                    }
326                    JPopupMenu popup = GuiUtils.makePopupMenu(items);
327                    popup.show(serverSelector, e.getX(), e.getY());
328                }
329            });
330        serverSelector.setMaximumRowCount(16);
331
332        groupSelector.setToolTipText("Right click to manage servers");
333        groupSelector.getEditor().getEditorComponent().addMouseListener(
334            new MouseAdapter() {
335                public void mouseReleased(MouseEvent e) {
336                    if (!SwingUtilities.isRightMouseButton(e)) {
337                        return;
338                    }
339
340                    AddeServer server = getAddeServer();
341                    if (server == null) {
342                        return;
343                    }
344                    List<JMenuItem> items = new ArrayList<JMenuItem>();
345
346                    // Set the right-click behavior
347                    if (isLocalServer()) {
348                        items.add(GuiUtils.makeMenuItem("Manage local ADDE data",
349                            AddeChooser.this, "doManager", null));
350                    }
351                    else {
352                        items.add(GuiUtils.makeMenuItem("Manage ADDE servers",
353                            AddeChooser.this, "doManager", null));
354                    }
355                    JPopupMenu popup = GuiUtils.makePopupMenu(items);
356                    popup.show(groupSelector, e.getX(), e.getY());
357                }
358            });
359        groupSelector.setMaximumRowCount(16);
360
361        //        serverManager = ((McIDASV)getIdv()).getServerManager();
362        //        serverManager.addManagedChooser(this);
363        addServerComp(descriptorLabel);
364        //        addServerComp(descriptorComboBox);
365
366        descriptorComboBox.addItemListener(new ItemListener() {
367            @Override public void itemStateChanged(ItemEvent e) {
368                if (!ignoreDescriptorChange
369                    && (e.getStateChange() == ItemEvent.SELECTED)) {
370                    descriptorChanged();
371                }
372            }
373        });
374
375        // Update the server list and load the saved state
376        updateServerList();
377        loadServerState();
378
379        // Default to no parameter button unless the overriding class wants one
380        hideParameterButton();
381    }
382
383    /**
384     * Force a reload of the available servers and groups.
385     */
386    public void updateServerList() {
387        updateServers();
388        updateGroups();
389    }
390
391    /**
392     * Returns a {@link java.util.Map Map} containing {@code user} and {@code proj}
393     * keys for the given {@code server/group} combination.
394     * 
395     * <p>The values are either the specific ADDE account details for 
396     * {@code server/group} or {@link edu.wisc.ssec.mcidasv.servermanager.AddeEntry#DEFAULT_ACCOUNT DEFAULT_ACCOUNT}
397     * values.
398     * 
399     * @param server Server name. Should not be {@code null}.
400     * @param group Group name on {@code name}. Should not be {@code null}.
401     * 
402     * @return {@code Map} containing the accounting details for {@code server/group}.
403     */
404    protected Map<String, String> getAccounting(final String server, final String group) {
405        Map<String, String> acctInfo = new HashMap<String, String>();
406        EntryStore entryStore = ((McIDASV)getIdv()).getServerManager();
407        String strType = this.getDataType();
408        EntryType type = strToEntryType(strType);
409        AddeAccount acct = entryStore.getAccountingFor(server, group, type);
410        acctInfo.put("user", acct.getUsername());
411        acctInfo.put("proj", acct.getProject());
412        return acctInfo;
413    }
414
415    /**
416     * Returns a {@link java.util.Map Map} containing {@code user} and {@code proj}
417     * keys for the given {@code server/group} combination.
418     * 
419     * <p>The values are either the specific ADDE account details for 
420     * {@code server/group} or {@link edu.wisc.ssec.mcidasv.servermanager.AddeEntry#DEFAULT_ACCOUNT DEFAULT_ACCOUNT}
421     * values.
422     * 
423     * @param server Server name. Should not be {@code null}.
424     * @param group Group name on {@code name}. Should not be {@code null}.
425     * 
426     * @return {@code Map} containing the accounting details for {@code server/group}.
427     */
428    protected Map<String, String> getAccounting(final AddeServer server, final String group) {
429        return getAccounting(server.getName(), group);
430    }
431
432    private List<AddeServer> getManagedServers(final String type) {
433        EntryStore entryStore = ((McIDASV)getIdv()).getServerManager();
434        return arrList(entryStore.getIdvStyleEntries(type));
435    }
436
437    /**
438     * Query the {@link EntryStore server manager} to determine the
439     * {@literal "format"} used by a given descriptor.
440     * 
441     * @param descriptor Local ADDE descriptor to check.
442     *                   Value can be {@code null}.
443     * 
444     * @return Either the format associated with the given {@code descriptor}
445     *         or {@link LocalAddeEntry.AddeFormat#INVALID}.
446     */
447    public LocalAddeEntry.AddeFormat getFormatFromDescriptor(String descriptor) {
448        return ((McIDASV)getIdv()).getServerManager().getLocalEntries().stream()
449                     .filter(entry -> Objects.equals(descriptor,
450                                                     entry.getDescriptor().toUpperCase()))
451                     .findFirst().map(LocalAddeEntry::getFormat)
452                     .orElse(LocalAddeEntry.AddeFormat.INVALID);
453    }
454    
455    /**
456     * Determine whether or not the specified {@code format} requires use of
457     * the absolute times tab.
458     * 
459     * <p>Thus far, only {@link LocalAddeEntry.AddeFormat#SCMI} needs this
460     * treatment.</p>
461     * 
462     * @param format Local ADDE {@literal "format"} to check.
463     * 
464     * @return Whether or not {@code format} should use <b>only</b> the
465     *         absolute times tab.
466     */
467    public boolean formatRequiresAbsolute(LocalAddeEntry.AddeFormat format) {
468        return format == LocalAddeEntry.AddeFormat.SCMI;
469    }
470
471    public void updateServers() {
472        Object selected = serverSelector.getSelectedItem();
473
474        String type = getGroupType();
475        List<AddeServer> managedServers = getManagedServers(type);
476        List<AddeServer> localList = arrList();
477        List<AddeServer> remoteList = arrList();
478        addeServers = CollectionHelpers.arrList();
479        for (AddeServer server : managedServers) {
480            if (server.getIsLocal())
481                localList.add(server);
482            else
483                remoteList.add(server);
484        }
485
486//        logger.debug("{}: updateServers: local size={} contents={}", new Object[] { getDataType(), localList.size(), localList });
487//        logger.debug("{}: updateServers: remote size={} contents={}", new Object[] { getDataType(), remoteList.size(), remoteList });
488
489        // server list doesn't need a separator if there's only remote servers
490        if (!localList.isEmpty()) {
491            addeServers.addAll(localList);
492            addeServers.add(new AddeServer(separator));
493        }
494        Comparator<AddeServer> byServer = new ServerComparator();
495        Collections.sort(remoteList, byServer);
496        addeServers.addAll(remoteList);
497
498        // always making this call helps to ensure the chooser stays up to date
499        // with the server manager.
500        GuiUtils.setListData(serverSelector, addeServers);
501        if (!addeServers.isEmpty()) {
502            if (selected == null || !containsServerName(addeServers, selected)) {
503                selected = serverSelector.getItemAt(0);
504//                logger.debug("updateServers: selecting item at idx=0, item={} chooser={}", selected, this.getDataType());
505            }
506            
507            int index = getSelectorIndex(selected, serverSelector);
508            serverSelector.setSelectedIndex(index);
509        }
510    }
511
512    /**
513     * Searches the given {@link java.util.List List} of {@link ucar.unidata.idv.chooser.adde.AddeServer AddeServers}
514     * for {@code server}.
515     * 
516     * @param servers Servers to search. {@code null} is permitted.
517     * @param server Server to search for within {@code servers}. {@code null} is permitted.
518     * 
519     * @return {@code true} if {@code servers} contains {@code server} or {@code false} otherwise.
520     */
521    protected static boolean containsServerName(final List<AddeServer> servers, final Object server) {
522        if (servers == null || server == null) {
523            return false;
524        }
525        String serverName = (server instanceof AddeServer) ? ((AddeServer)server).getName() : server.toString();
526        for (AddeServer tmp : servers) {
527            if (tmp.getName().equals(serverName)) {
528                return true;
529            }
530        }
531        return false;
532    }
533
534    /**
535     * Searches the given {@link java.util.List List} of {@link ucar.unidata.idv.chooser.adde.AddeServer.Group Groups}
536     * for {@code group}.
537     * 
538     * @param groups Groups to search. {@code null} is permitted.
539     * @param group Group to search for within {@code group}. {@code null} is permitted.
540     * 
541     * @return {@code true} if {@code groups} contains {@code group} or {@code false} otherwise.
542     */
543    protected static boolean containsGroupName(final List<Group> groups, final Object group) {
544        if (groups == null || group == null) {
545            return false;
546        }
547        String groupName = (group instanceof Group) ? ((Group)group).getName() : group.toString();
548        for (Group tmp : groups) {
549            if (tmp.getName().equals(groupName)) {
550                return true;
551            }
552        }
553        return false;
554    }
555
556    /**
557     * Sort the groups alphabetically
558     */
559    public void updateGroups() {
560        if (addingServer || groupSelector == null || getAddeServer() == null)
561            return;
562
563        Object selected = groupSelector.getSelectedItem();
564
565        EntryStore servManager = ((McIDASV)getIdv()).getServerManager();
566
567        List<Group> groups = CollectionHelpers.arrList();
568        if (isLocalServer()) {
569            groups.addAll(servManager.getIdvStyleLocalGroups());
570        } else {
571            String sel = null;
572            Object obj = serverSelector.getSelectedItem();
573            if (obj instanceof String) {
574                sel = (String)obj;
575//                logger.debug("updateGroups: string={} chooser={}", sel, this.getDataType());
576            } else if (obj instanceof AddeServer) {
577                sel = ((AddeServer)obj).getName();
578//                logger.debug("updateGroups: server selection={} chooser={}", sel, this.getDataType());
579            } else {
580                sel = obj.toString();
581//                logger.debug("updateGroups: unknown type={}; toString={}", sel.getClass().getName(), sel);
582            }
583
584            EntryType selType = strToEntryType(getGroupType());
585            groups.addAll(servManager.getIdvStyleRemoteGroups(sel, selType));
586        }
587//        logger.trace("updateGroups: selected={} (type={}) chooser={} contents={}", new Object[] { serverSelector.getSelectedItem(), serverSelector.getSelectedItem().getClass().getName(), this.getDataType(), groups});
588        Comparator<Group> byGroup = new GroupComparator();
589        Collections.sort(groups, byGroup);
590        GuiUtils.setListData(groupSelector, groups);
591        if (!groups.isEmpty()) {
592            if (selected == null || !containsGroupName(groups, selected)) {
593                selected = groupSelector.getItemAt(0);
594            }
595            groupSelector.setSelectedItem(selected);
596        }
597    }
598
599    /**
600     * Load any saved server state
601     */
602    @Override protected void loadServerState() {
603        if (addeServers == null) {
604//            logger.debug("loadServerState: addeServers == null chooser={}", this.getDataType());
605            return;
606        }
607        String[] serverState = getDefaultServerSelection();
608        if (serverState == null) {
609//            serverState = Constants.DEFAULT_SERVERSTATE;
610//            logger.debug("loadServerState: serverState == null chooser={}",this.getDataType());
611            return;
612        }
613        AddeServer server = AddeServer.findServer(addeServers, serverState[0]);
614        if (server == null) {
615//            logger.debug("loadServerState: server == null chooser={}",this.getDataType());
616            return;
617        }
618//        logger.debug("loadServerState: selecting server={} chooser={}", server, this.getDataType());
619        serverSelector.setSelectedItem(server);
620        setGroups();
621        updateGroups();
622        if (serverState[1] != null) {
623            Group group = new Group(getDataType(), serverState[1], serverState[1]);
624            int index = getSelectorIndex(group, groupSelector);
625            if (index >= 0) {
626//                logger.debug("loadServerState: selecting index={} group={} chooser={}", new Object[] { index, group, this.getDataType() });
627                groupSelector.setSelectedIndex(index);
628            } else {
629//                logger.debug("loadServerState: group == null chooser={}", this.getDataType());
630            }
631        } else {
632//            logger.debug("loadServerState: serverState[1] == null chooser={}", this.getDataType());
633        }
634    }
635
636    /**
637     * Decide if the server you're asking about is actually a separator
638     */
639    protected static boolean isSeparator(AddeServer checkServer) {
640        if (checkServer != null) {
641            if (checkServer.getName().equals(separator)) {
642                return true;
643            }
644        }
645        return false;
646    }
647
648    /**
649     * Decide if the server you're asking about is local
650     */
651    protected boolean isLocalServer() {
652        return isLocalServer(getAddeServer());
653    }
654
655    protected static boolean isLocalServer(AddeServer checkServer) {
656        if (checkServer != null) {
657            return checkServer.getIsLocal();
658        }
659        return false;
660    }
661
662    private void setBadServer(String name, String group) {
663        if (name == null) {
664            name = "";
665        }
666        if (group == null) {
667            group = "";
668        }
669
670        lastBadServer = name;
671        lastBadGroup = group;
672    }
673
674    private boolean isBadServer(String name, String group) {
675        assert lastBadServer != null;
676        assert lastBadGroup != null;
677        return lastBadServer.equals(name) && lastBadGroup.equals(group);
678    }
679
680    private void setLastServer(String name, String group, AddeServer server) {
681//        logger.trace("name='{}' group='{}' server='{}' old: name='{}' group='{}' server='{}'", new Object[] { name, group, server, lastServerName, lastServerGroup, lastServer });
682        if (name == null) {
683            name = "";
684        }
685        if (group == null) {
686            group = "";
687        }
688        if (server == null) {
689            server = new AddeServer(name);
690            Group addeGroup = new Group(getDataType(), group, group);
691            server.addGroup(addeGroup);
692        }
693        
694        lastServerName = name;
695        lastServerGroup = group;
696        lastServer = server;
697    }
698
699    private boolean isLastServer(String name, String group) {
700        assert lastServer != null;
701        assert lastServerName != null;
702        assert lastServerGroup != null;
703        return lastServerName.equals(name) && lastServerGroup.equals(group);
704    }
705
706    @EventSubscriber(eventClass=EntryStore.Event.class)
707    public void onServerManagerDataEvent(EntryStore.Event evt) {
708        EntryStore servManager = ((McIDASV)getIdv()).getServerManager();
709//        logger.debug("onServerManagerDataEvent: evt={} server={}", evt, servManager.getLastAdded());
710        this.updateServerList();
711    }
712
713    @EventSubscriber(eventClass=TabbedAddeManager.Event.class)
714    public void onServerManagerWindowEvent(TabbedAddeManager.Event evt) {
715//        logger.debug("onServerManagerWindowEvent: caught event bus obj");
716    }
717
718    private boolean addingServer = false;
719
720    /**
721     * Search a given {@link JComboBox} for the index of a given object. Mostly
722     * useful for searching {@link #serverSelector} or {@link #groupSelector}.
723     * 
724     * @param needle An object. {@code null} values are permitted.
725     * @param haystack {@code JComboBox} to search. {@code null} values are 
726     * permitted, but return {@code -1}.
727     * 
728     * @return Either the index of {@code needle} within {@code haystack}, or
729     * {@code -1} if {@code needle} could not be found (or {@code haystack} is 
730     * {@code null}).
731     */
732    protected static int getSelectorIndex(final Object needle, 
733        final JComboBox haystack) 
734    {
735        if (haystack == null) {
736            return -1;
737        }
738
739        String name = null;
740        if (needle instanceof AddeServer) {
741            name = ((AddeServer)needle).getName();
742        } else if (needle instanceof Group) {
743            name = ((Group)needle).getName();
744        } else if (needle instanceof AddeEntry) {
745            name = ((AddeEntry)needle).getAddress();
746        } else {
747            name = needle.toString();
748        }
749
750        if (isLoopback(name)) {
751            return 0;
752        }
753
754        for (int i = 0; i < haystack.getItemCount(); i++) {
755            Object item = haystack.getItemAt(i);
756            String tmpName;
757            if (item instanceof AddeServer) {
758                tmpName = ((AddeServer)item).getName();
759            } else {
760                tmpName = item.toString();
761            }
762
763            if (name.equals(tmpName)) {
764                return i;
765            }
766        }
767        return -1;
768    }
769
770    /**
771     * Get the selected AddeServer
772     *
773     * @return the server or null
774     */
775    protected AddeServer getAddeServer() {
776        if (lastServerName != null && lastServerName.equals("unset")) {
777            return null;
778        }
779
780        Object selected = serverSelector.getSelectedItem();
781        if ((selected != null) && (selected instanceof AddeServer)) {
782            AddeServer server = (AddeServer)selected;
783            String group = getGroup(true);
784            Map<String, String> accounting = getAccounting(server, group);
785//            logger.trace("accounting: new: u='{}' p='{}' old: u='{}' p='{}'", new Object[] { accounting.get("user"), accounting.get("proj"), lastServerUser, lastServerProj });
786            lastServerUser = accounting.get("user");
787            lastServerProj = accounting.get("proj");
788            setLastServer(server.getName(), group, server);
789            return (AddeServer)selected;
790        } else if ((selected != null) && (selected instanceof String)) {
791
792            EntryStore servManager = ((McIDASV)getIdv()).getServerManager();
793            String server = (String)selected;
794            String group = getGroup(true);
795
796            if (isBadServer(server, group)) {
797//                logger.trace("getAddeServer: returning null; known bad server; server={} group={}", server, group);
798                return null;
799            }
800
801            if (isLastServer(server, group)) {
802//                logger.trace("getAddeServer: returning last server name; server={} group={}", server, group);
803                return lastServer;
804            }
805
806            EditorAction editorAction = EditorAction.INVALID;
807            if (!isLoopback(server)) {
808                RemoteEntryEditor editor = new RemoteEntryEditor(servManager, server, "");
809                editor.setVisible(true);
810                editorAction = editor.getEditorAction();
811            } else {
812                LocalEntryEditor editor = new LocalEntryEditor(servManager, group);
813                editor.setVisible(true);
814                editorAction = editor.getEditorAction();
815            }
816
817            int servIndex = 0;
818            int groupIndex = 0;
819
820            if (editorAction != EditorAction.CANCELLED && editorAction != EditorAction.INVALID) {
821
822                List<AddeServer> added = arrList(EntryTransforms.convertMcvServers(servManager.getLastAddedByType(strToEntryType(getDataType()))));
823                AddeServer first = null;
824                if (!added.isEmpty()) {
825                    first = added.get(0);
826                    servIndex = getSelectorIndex(first, serverSelector);
827                    setLastServer(server, group, first);
828                } 
829
830                serverSelector.setSelectedIndex(servIndex);
831                groupSelector.setSelectedIndex(groupIndex);
832//                logger.trace("getAddeServer: serverIdx={} groupIdx={}", servIndex, groupIndex);
833
834                return first;
835            } else {
836//                logger.trace("getAddeServer: returning null due to cancel request");
837                setBadServer(server, group);
838                return null;
839            }
840
841            
842            
843        } else if (selected == null) {
844//            logger.trace("getAddeServer: null object in selector; returning null");
845        } else {
846//            logger.debug("getAddeServer: unknown obj type={}; toString={}", selected.getClass().getName(), selected.toString());
847        }
848        return null;
849    }
850
851    /**
852     * A utility to add a component to the list of components that
853     * need the descriptor
854     *
855     * @param comp The component
856     * @return The component
857     */
858    protected JComponent addDescComp(JComponent comp) {
859        compsThatNeedDescriptor.add(comp);
860        return comp;
861    }
862    
863    /**
864     * Set LABEL_SELECT from elsewhere
865     */
866    protected void setSelectString(String string) {
867        LABEL_SELECT = string;
868    }
869    
870    /**
871     * Reset the descriptor stuff
872     */
873    protected void resetDescriptorBox() {
874        ignoreDescriptorChange = true;
875        descriptorComboBox.setSelectedItem(LABEL_SELECT);
876        ignoreDescriptorChange = false;
877    }
878    
879    /**
880     * Handle when the user presses the connect button
881     *
882     * @throws Exception On badness
883     */
884    public void handleConnect() throws Exception {
885        AddeServer server = getAddeServer();
886        if (server == null) {
887            return;
888        }
889        setState(STATE_CONNECTING);
890        connectToServer();
891        handleUpdate();
892    }
893
894    /**
895     * Show the user a descriptive error message in a dialog (if in foreground
896     * mode) depending on the state of {@code e}.
897     *
898     * @param e Exception to handle. Cannot be {@code null}.
899     *
900     * @throws NullPointerException if {@code e} is {@code null}.
901     *
902     * @see #handleConnectionError(String, Exception)
903     */
904    @Override protected void handleConnectionError(Exception e) {
905        handleConnectionError("", e);
906    }
907
908    /**
909     * Show the user a descriptive error message (with optional details) in a
910     * dialog.
911     *
912     * @param details Details about the context of {@code e}. {@code null} will
913     * be treated as an empty {@code String}.
914     * @param e Exception to handle. Cannot be {@code null}.
915     *
916     * @throws NullPointerException if {@code e} is {@code null}.
917     */
918    protected void handleConnectionError(String details, Exception e) {
919        Objects.requireNonNull(e, "Cannot handle null exception");
920        logger.error("attempting to handle connection error", e);
921
922        if ((details != null) && !details.isEmpty()) {
923            details = details+":\n";
924        } else {
925            details = "";
926        }
927
928        boolean isError = true;
929        if (e.getMessage() != null) {
930            String msg = e.getMessage();
931            int msgPos = msg.indexOf("AddeURLException:");
932            if ((msgPos >= 0) && (msg.length() > 18)) {
933                msg = msg.substring(msgPos + 18);
934                GuiUtils.showDialog("ADDE Error", new JLabel(details+msg));
935            } else if (msg.indexOf("Connecting to server:localhost:") >= 0) {
936                GuiUtils.showDialog("ADDE Error", new JLabel("Local server is not responding."));
937            } else if (msg.toLowerCase().contains("unknownhostexception")) {
938                LogUtil.userErrorMessage("Could not access server: " + getServer());
939            } else if ((e instanceof AddeURLException) || msg.toLowerCase().contains("server unable to resolve this dataset")) {
940                handleUnknownDataSetError();
941            } else if ((msg.toLowerCase().contains("no images satisfy"))
942                || (msg.toLowerCase().contains("error generating list of files")))
943            {
944                LogUtil.userErrorMessage("No data available for the selection");
945                isError = false;
946            } else {
947                LogUtil.logException("Encountered a problem (server: '" + getServer()+"'):\n"+details, e);
948            }
949        } else {
950            LogUtil.userErrorMessage("Encountered a problem (server: '" + getServer() + "'):\n" + details +e);
951        }
952
953        if (isError && (getState() == STATE_CONNECTED)) {
954            setHaveData(false);
955            resetDescriptorBox();
956            updateStatus();
957            setState(STATE_UNCONNECTED);
958        }
959    }
960
961    /**
962     * Handle unknown data set error
963     */
964    @Override protected void handleUnknownDataSetError() {
965        String server = getServer();
966        String group = getGroup();
967        Map<String, String> acct = getAccounting(server, group);
968        String user = acct.get("user");
969        String proj = acct.get("proj");
970
971        StringBuilder msg = new StringBuilder("Could not connect to dataset \"");
972        msg.append(getGroup()).append("\" on server \"").append(getServer()).append("\".");
973        if (DEFAULT_ACCOUNT.getUsername().equals(user) && DEFAULT_ACCOUNT.getProject().equals(proj)) {
974            msg.append("\n\nDataset may require ADDE accounting information.");
975        } else {
976            msg.append("\n\nAccounting information:\nusername: \"")
977            .append(user).append("\"\nproject: \"").append(proj).append('"');
978        }
979        LogUtil.userErrorMessage(msg.toString());
980        setState(STATE_UNCONNECTED);
981    }
982
983    /**
984     * Handle the event
985     *
986     * @param ae The event
987     */
988    public void actionPerformed(ActionEvent ae) {
989        String cmd = ae.getActionCommand();
990        if (cmd.equals(CMD_MANAGER)) {
991            doManager();
992        }
993        else {
994            super.actionPerformed(ae);
995        }
996    }
997
998    /**
999     * Go directly to the Server Manager
1000     */
1001    public void doManager() {
1002//      if (isLocalServer()) {
1003//          ((McIDASV)getIdv()).showAddeManager();
1004//          return;
1005//      }
1006        getIdv().getPreferenceManager().showTab(Constants.PREF_LIST_ADDE_SERVERS);
1007    }
1008    
1009    /**
1010     * Show the parameter restore tree
1011     */
1012    public void doParameters() {
1013        JPopupMenu popup = new JPopupMenu();
1014        JMenuItem mi = new JMenuItem("Manage...");
1015        mi.addActionListener(new ActionListener() {
1016            public void actionPerformed(ActionEvent ae) {
1017                System.out.println(ae);
1018                showParameterSetDialog(getParameterSetType());
1019            }
1020        });
1021        popup.add(mi);
1022        
1023        // Add the checkbox to automatically create a data source
1024        cb.addActionListener(new ActionListener() {
1025            public void actionPerformed(ActionEvent ae) {
1026                shouldAddSource = cb.isSelected();
1027            }
1028        });
1029        popup.addSeparator();
1030        popup.add(cb);
1031
1032        final PersistenceManager pm = (PersistenceManager)getIdv().getPersistenceManager();
1033        List<ParameterSet> parameterSets = pm.getAllParameterSets(getParameterSetType());
1034
1035        for (int i=0; i<parameterSets.size(); i++) {
1036            if (i==0) popup.addSeparator();
1037            final ParameterSet ps = parameterSets.get(i);
1038            
1039            // Parameter set at root
1040            if (ps.getCategories().size() == 0) {
1041                mi = new JMenuItem(ps.getName());
1042                mi.addActionListener(new ActionListener() {
1043                    public void actionPerformed(ActionEvent ae) {
1044                        restoreParameterSet(ps.getElement());
1045                    }
1046                });
1047                popup.add(mi);
1048            }
1049            
1050            // Recurse into folders
1051            else {
1052                // Find or make the menu for the given parameter set
1053                JMenu m = getPopupSubMenuForParameterSet(popup, ps);
1054                // Create parameter set entry
1055                mi = new JMenuItem(ps.getName());
1056                mi.addActionListener(new ActionListener() {
1057                    public void actionPerformed(ActionEvent ae) {
1058                        restoreParameterSet(ps.getElement());
1059                    }
1060                });
1061                m.add(mi);
1062            }
1063            
1064        }
1065
1066        popup.show(parameterButton, 0, (int) parameterButton.getBounds().getHeight());
1067    }
1068    
1069    private JMenu getPopupSubMenuForParameterSet(JPopupMenu popup, final ParameterSet ps) {
1070        List<String> menuNames = ps.getCategories();
1071        if (menuNames.size() < 1) return null;
1072
1073        // Build the complete menu
1074        String menuName = menuNames.get(0);
1075        menuNames.remove(0);
1076        JMenu theMenu = new JMenu();
1077        
1078        // Look for the menu in popup
1079        boolean found = false;
1080        for (int i=0; i<popup.getComponentCount(); i++) {
1081            Component thisComponent = popup.getComponent(i);
1082            if (thisComponent instanceof JMenu && ((JMenu)thisComponent).getText().equals(menuName)) {
1083                theMenu = mergeMenuNames((JMenu)thisComponent, menuNames);
1084                found = true;
1085            }
1086        }
1087        
1088        // Make a new menu, add the root, return the leaf
1089        if (!found) {
1090            JMenu theRoot = new JMenu(menuName);
1091            theMenu = makeMenuRecursive(theRoot, menuNames);
1092            popup.add(theRoot);
1093        }
1094        
1095        return theMenu;
1096    }
1097    
1098    /**
1099     * Make a new recursive menu
1100     * 
1101     * @param rootMenu The root menu to add items to
1102     * @param menuNames List of string names for submenus
1103     * @return A new JMenu representing the leaf
1104     */
1105    private JMenu makeMenuRecursive(JMenu rootMenu, List<String> menuNames) {
1106        if (menuNames.size() < 1) return rootMenu;
1107        JMenu newMenu = new JMenu(menuNames.get(0));
1108        rootMenu.add(newMenu);
1109        menuNames.remove(0);
1110        return makeMenuRecursive(newMenu, menuNames);
1111    }
1112    
1113    /**
1114     * Recurse into a menu, returning either a pointer to the designated names path
1115     *  or a pointer to the leaf menu added by merging new names
1116     * 
1117     * @param thisMenu The root menu to merge
1118     * @param menuNames List of string names to look for
1119     * @return A new JMenu representing the leaf matched by menuNames
1120     */
1121    private JMenu mergeMenuNames(JMenu thisMenu, List<String> menuNames) {
1122        if (menuNames.size() < 1) return thisMenu;
1123        boolean found = false;
1124        String menuName = menuNames.get(0);
1125        for (int i=0; i<thisMenu.getItemCount(); i++) {
1126            JMenuItem mi = thisMenu.getItem(i);
1127            if (!(mi instanceof JMenu)) continue;
1128            if (mi.getText().equals(menuName)) {
1129                menuNames.remove(0);
1130                thisMenu = mergeMenuNames((JMenu)mi, menuNames);
1131                found = true;
1132            }
1133        }
1134        if (!found) {
1135            thisMenu = makeMenuRecursive(thisMenu, menuNames);
1136        }
1137        return thisMenu;
1138    }
1139    
1140    /**
1141     * Return the parameter type associated with this chooser.  Override!
1142     */
1143    protected String getParameterSetType() {
1144        return "adde";
1145    }
1146    
1147    /**
1148     * Show the parameter set manager.
1149     */
1150    private void showParameterSetDialog(final String parameterSetType) {
1151        ParameterTree tree = (ParameterTree) parameterTrees.get(parameterSetType);
1152        if (tree == null) {
1153            tree = new ParameterTree((UIManager)getIdv().getIdvUIManager() , parameterSetType);
1154            parameterTrees.put(parameterSetType, tree);
1155        }
1156        else {
1157            //DAVEP
1158            System.out.println("Should refresh the parameter tree here");
1159        }
1160        tree.setVisible(true);
1161    }
1162
1163    /**
1164     * Clear the selected parameter set.
1165     */
1166    protected void clearParameterSet() {
1167        restoreElement = null;
1168        restoreTimes = new ArrayList(); 
1169        shouldAddSource = false;
1170    }
1171
1172    /**
1173     * Restore the selected parameter set using element attributes.
1174     * 
1175     * @param restoreElement {@code Element} with the desired attributes.
1176     * {@code null} values are permitted.
1177     *
1178     * @return {@code true} if the parameter set was restored, {@code false}
1179     * otherwise.
1180     */
1181    protected boolean restoreParameterSet(Element restoreElement) {
1182        if (restoreElement == null) return false;
1183        if (!restoreElement.getTagName().equals("default")) return false;
1184
1185        this.restoreElement = restoreElement;
1186
1187        boolean oldISCE = ignoreStateChangedEvents;
1188        ignoreStateChangedEvents = true;
1189
1190        // Restore server
1191        String server = restoreElement.getAttribute(ATTR_SERVER);
1192        if (server != null) serverSelector.setSelectedItem(new AddeServer(server));
1193
1194        // Restore group
1195        String group = restoreElement.getAttribute(ATTR_GROUP);
1196        if (group != null) groupSelector.setSelectedItem(group);
1197
1198        // Act as though the user hit "connect"
1199        readFromServer();
1200
1201        // Restore descriptor
1202        String descriptor = restoreElement.getAttribute(ATTR_DESCRIPTOR);
1203        if (descriptor != null) {
1204            Enumeration enumeration = descriptorTable.keys();
1205            for (int i = 0; enumeration.hasMoreElements(); i++) {
1206                String key = enumeration.nextElement().toString();
1207                Object val = descriptorTable.get(key);
1208                if (descriptor.equals(val)) {
1209                    descriptorComboBox.setSelectedItem(val + nameSeparator + key);
1210                    descriptorChanged();
1211                    break;
1212                }
1213            } 
1214        }
1215
1216        // Restore date/time
1217        if (restoreElement.hasAttribute(ATTR_POS)) {
1218            setDoAbsoluteTimes(false);
1219            Integer pos = new Integer(restoreElement.getAttribute(ATTR_POS));
1220            if (pos.intValue() >= 0) {
1221                getRelativeTimesList().setSelectedIndex(pos);
1222            }
1223            restoreTimes = new ArrayList(); 
1224        }
1225        else if ((restoreElement.hasAttribute(ATTR_DAY)) && (restoreElement.hasAttribute(ATTR_TIME))) {
1226            setDoAbsoluteTimes(true);
1227            String dateStr = restoreElement.getAttribute(ATTR_DAY);
1228            String timeStr = restoreElement.getAttribute(ATTR_TIME);
1229            List dateS = StringUtil.split(dateStr, ",");
1230            List timeS = StringUtil.split(timeStr, ",");
1231            int numImages = timeS.size();
1232            restoreTimes = new ArrayList(); 
1233            try {
1234                DateTime dt = new DateTime();
1235                dt.resetFormat();
1236                String dtformat = dt.getFormatPattern();
1237                for (int ix=0; ix<numImages; ix++) {
1238                    DateTime restoreTime = dt.createDateTime((String)dateS.get(ix) + " " + (String)timeS.get(ix));
1239                    restoreTimes.add(restoreTime);
1240                }
1241            } catch (Exception e) {
1242                System.out.println("Exception e=" + e);
1243                return false;
1244            }
1245        }
1246
1247        System.out.println("Returning from AddeChooser.restoreParameterSet()");
1248
1249        ignoreStateChangedEvents = oldISCE;
1250        return true;
1251    }
1252
1253    /**
1254     * Set the absolute times list. The times list can contain any of the object types
1255     * that makeDatedObjects knows how to handle, i.e., Date, visad.DateTime, DatedThing, AddeImageDescriptor, etc.
1256     *
1257     * @param times List of thinggs to put into absolute times list
1258     */
1259    protected void setAbsoluteTimes(List times) {
1260        super.setAbsoluteTimes(times);
1261        restoreAbsoluteTimes();
1262    }
1263
1264    protected void restoreAbsoluteTimes() {
1265        List allTimes = makeDatedObjects(super.getAbsoluteTimes());
1266        if (restoreTimes.size() > 0 && allTimes.size() > 0) {
1267            int[] indices  = new int[restoreTimes.size()];
1268            try {
1269                DateTime rtdt;
1270                DateTime atdt;
1271                DatedThing at;
1272                for (int i = 0; i < restoreTimes.size(); i++) {
1273                    rtdt = (DateTime)restoreTimes.get(i);
1274                    for (int j = 0; j < allTimes.size(); j++) {
1275                        at = (DatedThing)allTimes.get(j);
1276                        atdt = new DateTime(at.getDate());
1277                        if (atdt.equals(rtdt)) {
1278                            indices[i] = j;
1279                        }
1280                    }
1281                }
1282            } catch (Exception e) {
1283                System.out.println("Exception e=" + e);
1284            }
1285            setSelectedAbsoluteTimes(indices);
1286        }
1287    }
1288
1289    /**
1290     * show/hide the parameter restore button
1291     */
1292    public void showParameterButton() {
1293        parameterButton.setVisible(true);
1294    }
1295
1296    public void hideParameterButton() {
1297        parameterButton.setVisible(false);
1298    }
1299
1300    /**
1301     * Override and simulate clicking Add Source if requested
1302     */
1303    public void setHaveData(boolean have) {
1304        super.setHaveData(have);
1305        if (have && shouldAddSource) {
1306            // Even though setHaveData should mean we can go, we can't... wait a few jiffies
1307            Misc.runInABit(100, AddeChooser.this, "doClickLoad", null);
1308        }
1309    }
1310
1311    public void doClickLoad() {
1312        loadButton.doClick();
1313    }
1314
1315    public void showServers() {
1316        allServersFlag = !allServersFlag;
1317        XmlObjectStore store = getIdv().getStore();
1318        store.put(Constants.PREF_SYSTEMSERVERSIMG, allServersFlag);
1319        store.save();
1320        updateServers();
1321        updateGroups();
1322    }
1323
1324    protected String getStateString() {
1325        int state = getState();
1326        switch (state) {
1327            case STATE_CONNECTED: return "Connected to server";
1328            case STATE_UNCONNECTED: return "Not connected to server";
1329            case STATE_CONNECTING: return "Connecting to server";
1330            default: return "Unknown state: " + state;
1331        }
1332    }
1333
1334    /**
1335     * Disable/enable any components that depend on the server.
1336     * Try to update the status label with what we know here.
1337     */
1338    protected void updateStatus() {
1339        super.updateStatus();
1340        if (getState() == STATE_CONNECTED) {
1341            lastServer = new AddeServer("");
1342            lastServerGroup = "";
1343            lastServerName = "";
1344            lastServerProj = "";
1345            lastServerUser = "";
1346
1347            if (!haveDescriptorSelected()) {
1348                if (!usingStations() || haveStationSelected()) {
1349                    //                String name = getDataName().toLowerCase();
1350                    String name = getDescriptorLabel().toLowerCase();
1351                    if (StringUtil.startsWithVowel(name)) {
1352                        setStatus("Please select an " + name);
1353                    } else {
1354                        setStatus("Please select a " + name);
1355                    }
1356                }
1357            }
1358        }
1359
1360        GuiUtils.enableTree(connectButton, getState() != STATE_CONNECTING);
1361    }
1362    
1363    /**
1364     * Get the data type ID
1365     *
1366     * @return  the data type
1367     */
1368    public String getDataType() {
1369        return "ANY";
1370    }
1371
1372    /**
1373     * Check if the server is ok
1374     *
1375     * @return status code
1376     */
1377    protected int checkIfServerIsOk() {
1378        EntryStore servManager = ((McIDASV)getIdv()).getServerManager();
1379        if (isLocalServer() && !servManager.checkLocalServer()) {
1380                LogUtil.userErrorMessage("Local servers are stopped.\n\nLocal servers can be restarted from the 'Tools' menu:\n  Tools > Manage ADDE Datasets >\nLocal Servers > Start Local Servers");
1381                logger.info("Local servers are stopped");
1382            return STATUS_ERROR;
1383        }
1384        try {
1385            StringBuffer buff = getUrl(REQ_TEXT);
1386            appendKeyValue(buff, PROP_FILE, FILE_PUBLICSRV);
1387
1388            // TJJ Sep 2024
1389            // Some ADDE servers return an Invalid Accounting error if they can't find a PUBLIC.SRV file
1390            // This should not result in an error, it should not imply the accounting is not valid
1391            if (buff.toString().toUpperCase().contains("PUBLIC.SRV")) {
1392                // We are requesting the PUBLIC.SRV file
1393                logger.info("bailing out early, no PUBLIC.SRV");
1394                return STATUS_OK;
1395            }
1396            URLConnection urlc = IOUtil.getUrlConnection(buff.toString());
1397            InputStream   is   = urlc.getInputStream();
1398            is.close();
1399            return STATUS_OK;
1400        } catch (AddeURLException ae) {
1401            String aes = ae.toString();
1402            if (aes.indexOf("Invalid project number") >= 0 ||
1403                aes.indexOf("Invalid user id") >= 0 ||
1404                aes.indexOf("Accounting data") >= 0) {
1405                LogUtil.userErrorMessage("Invalid login.\n\nPlease verify your username and password.");
1406                logger.info("Invalid login");
1407                setState(STATE_UNCONNECTED);
1408                setHaveData(false);
1409                resetDescriptorBox();
1410                return STATUS_NEEDSLOGIN;
1411            }
1412            if (aes.indexOf("cannot run server 'txtgserv'") >= 0) {
1413                return STATUS_OK;
1414            }
1415            LogUtil.userErrorMessage("Error connecting to server " + getServer() + ":\n" + ae.getMessage());
1416            logger.info("Error connecting to server");
1417            setState(STATE_UNCONNECTED);
1418            setHaveData(false);
1419            resetDescriptorBox();
1420            return STATUS_ERROR;
1421        } catch (ConnectException exc) {
1422            setState(STATE_UNCONNECTED);
1423            setHaveData(false);
1424            resetDescriptorBox();
1425            String message = "Error connecting to server " + getServer();
1426            String info = "Error connecting to server";
1427            if (isLocalServer()) {
1428                if (!servManager.checkLocalServer()) {
1429                    message += "\n\nLocal servers can be restarted from the 'Tools' menu:\n  Tools > Manage ADDE Datasets >\n Local Servers > Start Local Servers";                     
1430                    info += " (Local servers are stopped)";
1431                }
1432                else {
1433                    message += "\n\nLocal servers appear to be running.\nYour firewall may be preventing access.";
1434                    info += " (Local servers are running)";
1435                }
1436            }
1437            LogUtil.userErrorMessage(message);
1438            logger.info(info);
1439            return STATUS_ERROR;
1440        } catch (EOFException exc) {
1441            setState(STATE_UNCONNECTED);
1442            setHaveData(false);
1443            resetDescriptorBox();
1444            LogUtil.userErrorMessage("Server " + getServer() + " is not responding");
1445            logger.info("Server is not responding");
1446            return STATUS_ERROR;
1447        } catch (Exception exc) {
1448            setState(STATE_UNCONNECTED);
1449            setHaveData(false);
1450            resetDescriptorBox();
1451            logException("Connecting to server: " + getServer(), exc);
1452            logger.info("Error connecting to server");
1453            return STATUS_ERROR;
1454        }
1455    }
1456
1457    public boolean canAccessServer() {
1458        return (checkIfServerIsOk() == STATUS_OK);
1459    }
1460
1461    public Map<String, String> getAccountingInfo() {
1462        AddeServer server = getAddeServer();
1463        Map<String, String> map = new LinkedHashMap<>(4);
1464        if (server != null) {
1465            List<AddeServer.Group> groups = cast(server.getGroups());
1466            Map<String, String>acctInfo = getAccounting(server, groups.get(0).toString());
1467            map.put("user", acctInfo.get("user"));
1468            map.put("proj", acctInfo.get("proj"));
1469            map.put("server", server.getName());
1470            map.put("group", getGroup());
1471        } else {
1472            map.put("user", RemoteAddeEntry.DEFAULT_ACCOUNT.getUsername());
1473            map.put("proj", RemoteAddeEntry.DEFAULT_ACCOUNT.getUsername());
1474            map.put("server", "");
1475            map.put("group", "");
1476        }
1477        return map;
1478    }
1479
1480    /**
1481     * Saves the currently selected server and group to a chooser-specific 
1482     * preference. Preference ID is {@code PREF_SERVERSTATE+'.'+getId()}.
1483     */
1484    @Override public void saveServerState() {
1485        String[] serverState = { getServer(), getGroup() };
1486        getIdv().getStore().put(PREF_SERVERSTATE+'.'+getId(), serverState);
1487        getIdv().getStore().save();
1488    }
1489
1490    /**
1491     * Connect to the server.
1492     */
1493    protected void connectToServer() {
1494        clearParameterSet();
1495        setDescriptors(null);
1496        setDoAbsoluteTimes(false);
1497        if (!canAccessServer()) {
1498            return;
1499        }
1500        readFromServer();
1501        saveServerState();
1502        ignoreStateChangedEvents = true;
1503        if (descList != null) {
1504            descList.saveState(groupSelector);
1505        }
1506        ignoreStateChangedEvents = false;
1507    }
1508
1509    /**
1510     * Do server connection stuff... override this with type-specific methods
1511     */
1512    protected void readFromServer() {
1513        readDescriptors();
1514        readTimes();
1515    }
1516
1517//    what the request needs to look like:
1518//    adde://localhost:8112/imagedata?&PORT=112&COMPRES S=gzip&USER=idv&PROJ=0
1519//        &VERSION=1&DEBUG=false&TRAC E=0&GROUP=MYDATA&DESCRIPTOR=ENTRY4&BAND=1
1520//        &LATLON= 30.37139 71.74912&PLACE=CENTER&SIZE=1000 1000&UNI T=BRIT
1521//        &MAG=1 1&SPAC=1&NAV=X&AUX=YES&DOC=X&POS=0
1522
1523    /**
1524     *  Generate a list of image descriptors for the descriptor list.
1525     */
1526    protected void readDescriptors() {
1527        try {
1528            StringBuffer buff = getGroupUrl(REQ_DATASETINFO, getGroup());
1529            buff.append("&type=").append(getDataType());
1530            logger.debug("readDesc: buff={}", buff.toString());
1531            DataSetInfo  dsinfo = new DataSetInfo(buff.toString());
1532            
1533            descriptorTable = dsinfo.getDescriptionTable();
1534            descriptorList.clear();
1535            commentList.clear();
1536            descriptorList.addAll(dsinfo.getDescriptorList());
1537            commentList.addAll(dsinfo.getCommentList());
1538            int count = commentList.size();
1539            String[] names = new String[count];
1540            for (int i = 0; i < count; i++) {
1541                if (!isLocalServer()) {
1542                    names[i] = descriptorList.get(i) + nameSeparator + commentList.get(i);
1543                } else {
1544                    names[i] = commentList.get(i);
1545                }
1546            }
1547            logger.debug("readDesc: names={}", names);
1548            Arrays.sort(names);
1549            SwingUtilities.invokeLater(() -> {
1550                setDescriptors(names);
1551                setState(STATE_CONNECTED);
1552            });
1553        } catch (Exception e) {
1554            handleConnectionError(e);
1555        }
1556    }
1557
1558    /**
1559     * Initialize the descriptor list from a list of names
1560     *
1561     * @param names  list of names
1562     */
1563    protected void setDescriptors(String[] names) {
1564        synchronized (WIDGET_MUTEX) {
1565            ignoreDescriptorChange = true;
1566            descriptorComboBox.removeAllItems();
1567            descriptorNames = names;
1568            if ((names == null) || (names.length == 0)) {
1569                return;
1570            }
1571            descriptorComboBox.addItem(LABEL_SELECT);
1572            for (int j = 0; j < names.length; j++) {
1573                logger.trace("adding names[{}]='{}' to combo box", j, names[j]);
1574                descriptorComboBox.addItem(names[j]);
1575            }
1576            ignoreDescriptorChange = false;
1577        }
1578    }
1579
1580    /**
1581     * Respond to a change in the descriptor list.
1582     */
1583    protected void descriptorChanged() {
1584        if (haveDescriptorSelected()) {
1585            LocalAddeEntry.AddeFormat format =
1586                getFormatFromDescriptor(getDescriptor());
1587            if (formatRequiresAbsolute(format)) {
1588                setDoAbsoluteTimes(true);
1589            }
1590            readTimes();
1591            updateStatus();
1592        }
1593    }
1594
1595    /**
1596     * Check if a descriptor (image type) has been chosen
1597     *
1598     * @return  true if an image type has been chosen
1599     */
1600    protected boolean haveDescriptorSelected() {
1601        if (!GuiUtils.anySelected(descriptorComboBox)) {
1602            return false;
1603        }
1604        return getDescriptor() != null;
1605    }
1606
1607    /**
1608     * Get the selected descriptor.
1609     *
1610     * @return  the currently selected descriptor.
1611     */
1612    protected String getDescriptor() {
1613        return getDescriptorFromSelection(getSelectedDescriptor());
1614    }
1615    
1616    /**
1617     * Get the descriptor relating to the selection.
1618     *
1619     * @param selection String name from the widget. Can be {@code null}.
1620     *
1621     * @return Either the descriptor associated with {@code selection} or {@code null} if {@link #descriptorTable} or 
1622     * {@code selection} is {@code null}.
1623     */
1624    protected String getDescriptorFromSelection(String selection) {
1625        if (descriptorTable == null) {
1626            return null;
1627        }
1628        if (selection == null) {
1629            return null;
1630        }
1631        
1632        String descriptor = null;
1633        if (!selection.contains(nameSeparator)) {
1634            descriptor = (String)descriptorTable.get(selection);
1635        } else {
1636            String[] toks = selection.split(nameSeparator, 2);
1637            String firstToken = toks[0].trim();
1638            if (descriptorList.contains(firstToken)) {
1639                descriptor = firstToken;
1640            } else {
1641                String key = toks[1].trim();
1642                descriptor = (String)descriptorTable.get(key);
1643            }
1644        }
1645        return descriptor;
1646    }
1647    
1648    /**
1649     * Get the selected descriptor.
1650     *
1651     * @return the selected descriptor
1652     */
1653    public String getSelectedDescriptor() {
1654        String selection = (String)descriptorComboBox.getSelectedItem();
1655        if (selection == null) {
1656            return null;
1657        }
1658        if (selection.equals(LABEL_SELECT)) {
1659            return null;
1660        }
1661        return selection;
1662    }
1663
1664    /**
1665     * Get the descriptor table for this chooser
1666     *
1667     * @return a Hashtable of descriptors and names
1668     */
1669    public Hashtable getDescriptorTable() {
1670        return descriptorTable;
1671    }
1672
1673    /**
1674     * Get any extra key=value pairs that are appended to all requests.
1675     *
1676     * @param buff The buffer to append onto
1677     */
1678    protected void appendMiscKeyValues(StringBuffer buff) {
1679        appendKeyValue(buff, PROP_COMPRESS, DEFAULT_COMPRESS);
1680        appendKeyValue(buff, PROP_PORT, getPort());
1681        // appendKeyValue(buff, PROP_DEBUG, DEFAULT_DEBUG);
1682        appendKeyValue(buff, PROP_DEBUG, Boolean.toString(EntryStore.isAddeDebugEnabled(false)));
1683        appendKeyValue(buff, PROP_VERSION, DEFAULT_VERSION);
1684        appendKeyValue(buff, PROP_USER, getLastAddedUser());
1685        appendKeyValue(buff, PROP_PROJ, getLastAddedProj());
1686    }
1687    
1688    /**
1689     * Return the ADDE port to use.
1690     * 
1691     * <p>Overridden by McIDAS-V because {@literal "local ADDE"} requires us to
1692     * use essentially arbitrary port numbers, so for local ADDE connections
1693     * we need to use {@link EntryStore#getLocalPort()}.</p>
1694     * 
1695     * @return ADDE port to use.
1696     */
1697    @Override protected String getPort() {
1698        if (isLocalServer()) {
1699            return EntryStore.getLocalPort();
1700        } else {
1701            return super.getPort();
1702        }
1703    }
1704    
1705    public String getLastAddedUser() {
1706        if ((lastServerUser != null) && !lastServerUser.isEmpty()) {
1707            logger.debug("getLastAddedUser: using non-default {}", lastServerUser);
1708            return lastServerUser;
1709        } else {
1710            logger.debug("getLastAddedUser: using default {}", DEFAULT_USER);
1711            return DEFAULT_USER;
1712        }
1713    }
1714
1715    public String getLastAddedProj() {
1716       if ((lastServerProj != null) && !lastServerProj.isEmpty()) {
1717            logger.debug("getLastAddedProj: using non-default {}", lastServerProj);
1718            return lastServerProj;
1719        } else {
1720            logger.debug("getLastAddedProj: using default {}", DEFAULT_PROJ);
1721            return DEFAULT_PROJ;
1722        }
1723    }
1724
1725    /**
1726     * Show the groups dialog.  This method is not meant to be called
1727     * but is public by reason of implementation (or insanity).
1728     */
1729    public void showGroups() {
1730        JPopupMenu popup = new JPopupMenu();
1731        popup.add(new JMenuItem("Reading public datasets..."));
1732
1733
1734        List groups = readGroups();
1735        popup.removeAll();
1736        if ((groups == null) || (groups.isEmpty())) {
1737            popup.add(new JMenuItem("The list of public datasets is not available"));
1738            popup.show(publicButton, 0, (int) publicButton.getBounds().getHeight());
1739            return;
1740        }
1741
1742        JMenuItem mi;
1743        for (int i = 0; i < groups.size(); i++) {
1744            final String group = groups.get(i).toString();
1745            mi = new JMenuItem(group);
1746            mi.addActionListener(ae -> {
1747                EntryStore servManager = ((McIDASV)getIdv()).getServerManager();
1748
1749                servManager.addEntry(
1750                    new RemoteAddeEntry.Builder(lastServerName, group)
1751                                       .account(lastServerUser, lastServerProj)
1752                                       .type(strToEntryType(getDataType()))
1753                                       .source(EntrySource.USER)
1754                                       .validity(EntryValidity.VERIFIED)
1755                                       .build());
1756
1757                groupSelector.setSelectedItem(group);
1758                doConnect();
1759            });
1760            popup.add(mi);
1761        }
1762        MenuScroller foo = new MenuScroller(popup, 125);
1763        foo.setParent(publicButton);
1764        popup.show(publicButton, 0, (int) publicButton.getBounds().getHeight());
1765    }
1766
1767    /**
1768     * return the String id of the chosen server name
1769     *
1770     * @return  the server name
1771     */
1772    public String getServer() {
1773        AddeServer server = getAddeServer();
1774        if (server != null) {
1775            return server.getName();
1776        } else {
1777            return "";
1778        }
1779    }
1780
1781    protected String getGroup() {
1782        return getGroup(false);
1783    }
1784
1785    /**
1786     * Is the group selector editable?
1787     *
1788     * @return Always returns {@code true}.
1789     */
1790    protected boolean isGroupEditable() {
1791        return true;
1792    }
1793
1794    /**
1795     * Get the image group from the GUI.
1796     *
1797     * @return The image group.
1798     */
1799    protected String getGroup(final boolean fromGetServer) {
1800        Object selected = groupSelector.getSelectedItem();
1801        if (selected == null) {
1802            return null;
1803        }
1804
1805        if (selected instanceof AddeServer.Group) {
1806            AddeServer.Group group = (AddeServer.Group) selected;
1807            return group.getName();
1808        }
1809
1810        if (selected instanceof String) {
1811            return (String)selected;
1812        }
1813
1814        String groupName = selected.toString().trim();
1815        if (!fromGetServer && (!groupName.isEmpty())) {
1816            //Force the get in case they typed a server name
1817            getServer();
1818
1819            AddeServer server = getAddeServer();
1820            if (server != null) {
1821                AddeServer.Group group =
1822                    getIdv().getIdvChooserManager().addAddeServerGroup(
1823                        server, groupName, getGroupType());
1824                if (!group.getActive()) {
1825                    getIdv().getIdvChooserManager().activateAddeServerGroup(
1826                        server, group);
1827                }
1828                //Now put the list of groups back in to the selector
1829                setGroups();
1830                groupSelector.setSelectedItem(group);
1831            }
1832        }
1833        return groupName;
1834    }
1835
1836    /**
1837     * Get the server selector
1838     * @return The server selector
1839     */
1840    public JComboBox getServerSelector() {
1841        if (serverSelector == null)
1842            serverSelector = super.getServerSelector();
1843
1844        ItemListener[] ell = serverSelector.getItemListeners();
1845        for (int i=0; i < ell.length; i++) {
1846            serverSelector.removeItemListener((ItemListener)ell[i]);
1847        }
1848        updateServers();
1849        updateGroups();
1850        serverSelector.addItemListener(new ItemListener() {
1851            public void itemStateChanged(ItemEvent e) {
1852                if (!ignoreStateChangedEvents) {
1853                    Object selected = serverSelector.getSelectedItem();
1854                    if (selected instanceof AddeServer) {
1855                        AddeServer selectedServer = (AddeServer)selected;
1856                        if (selectedServer != null) {
1857                            if (isSeparator(selectedServer)) {
1858                                connectButton.setEnabled(false);
1859                                return;
1860                            }
1861                        }
1862                    }
1863                    setState(STATE_UNCONNECTED);
1864                    connectButton.setEnabled(true);
1865//                    setGroups();
1866                    resetDescriptorBox();
1867                    updateGroups();
1868//                    System.err.println("itemStateChanged");
1869                }
1870//                else {
1871//                  System.out.println("Ignoring state change here...");
1872//                }
1873            }
1874        });
1875
1876        serverSelector.getEditor().getEditorComponent().addKeyListener(new KeyListener() {
1877            public void keyTyped(final KeyEvent e) {}
1878            public void keyPressed(final KeyEvent e) {}
1879            public void keyReleased(final KeyEvent e) {
1880                JTextField field = (JTextField)serverSelector.getEditor().getEditorComponent();
1881                boolean partialMatch = false;
1882                for (int i = 0; i < serverSelector.getItemCount(); i++) {
1883                    String entry = serverSelector.getItemAt(i).toString();
1884                    if (entry.toLowerCase().startsWith(field.getText().toLowerCase()))
1885                        partialMatch = true;
1886                }
1887
1888                if (!partialMatch && groupSelector != null) {
1889                    logger.debug("aha! chooser=", getDataType());
1890                    ((JTextField)groupSelector.getEditor().getEditorComponent()).setText("");
1891                }
1892            }
1893        });
1894
1895        return serverSelector;
1896    }
1897
1898    /**
1899     * Enable or disable the GUI widgets based on what has been
1900     * selected.
1901     */
1902    protected void enableWidgets() {
1903        synchronized (WIDGET_MUTEX) {
1904            boolean newEnabledState = (getState() == STATE_CONNECTED);
1905            for (int i = 0; i < compsThatNeedDescriptor.size(); i++) {
1906                JComponent comp = (JComponent) compsThatNeedDescriptor.get(i);
1907                if (comp.isEnabled() != newEnabledState) {
1908                    GuiUtils.enableTree(comp, newEnabledState);
1909                }
1910            }
1911            if (drivercbx != null) {
1912                boolean descriptorState = newEnabledState && haveDescriptorSelected();
1913//                logger.trace("hrm set drivercbx={}", anyTimeDrivers() && descriptorState);
1914                drivercbx.setEnabled(anyTimeDrivers() && descriptorState);
1915            }
1916        }
1917    }
1918    
1919    /**
1920     * Add a listener to the given combobox that will set the
1921     * state to unconnected
1922     *
1923     * @param box The box to listen to.
1924     */
1925    protected void clearOnChange(final JComboBox box) {
1926        box.addItemListener(new ItemListener() {
1927            public void itemStateChanged(ItemEvent e) {
1928                if ( !ignoreStateChangedEvents) {
1929                    setState(STATE_UNCONNECTED);
1930                    GuiUtils.setListData(descriptorComboBox, new Vector());
1931//                    System.err.println("clearOnChange");
1932                }
1933//                else {
1934//                  System.out.println("Ignoring state change in clearOnChange for: " + box.toString());
1935//                }
1936            }
1937        });
1938    }
1939
1940    /**
1941     * Get the descriptor widget label
1942     *
1943     * @return  label for the descriptor  widget
1944     */
1945    public String getDescriptorLabel() {
1946        return "Descriptor";
1947    }
1948
1949    protected int getNumTimesToSelect() {
1950        return 5;
1951    }
1952
1953    /**
1954     * Get the default selected index for the relative times list.
1955     *
1956     * @return default index
1957     */
1958    protected int getDefaultRelativeTimeIndex() {
1959        return 4;
1960    }
1961
1962    /**
1963     * Check the times lists
1964     */
1965    protected void checkTimesLists() {
1966        super.checkTimesLists();
1967        if (timesCardPanelExtra == null) {
1968            return;
1969        }
1970        if (getDoAbsoluteTimes()) {
1971            timesCardPanelExtra.show("absolute");
1972        } else {
1973            timesCardPanelExtra.show("relative");
1974        }
1975    }
1976
1977    /** Card panel to hold extra relative and absolute time components */
1978    protected GuiUtils.CardLayoutPanel timesCardPanelExtra;
1979
1980    /**
1981     * Set the relative and absolute extra components.
1982     */
1983    protected JPanel makeTimesPanel(JComponent relativeCard, JComponent absoluteCard) {
1984//        JPanel timesPanel = super.makeTimesPanel(false, true);
1985        JPanel timesPanel = super.makeTimesPanel(false, true, getIdv().getUseTimeDriver());
1986
1987        // Make a new timesPanel that has extra components tacked on the bottom, inside the tabs
1988        Component[] comps = timesPanel.getComponents();
1989
1990//        if (drivercbx != null) {
1991//            drivercbx.setEnabled(anyTimeDrivers());
1992//        }
1993
1994        if ((comps.length == 1) && (comps[0] instanceof JTabbedPane)) {
1995            timesCardPanelExtra = new GuiUtils.CardLayoutPanel();
1996            if (relativeCard == null) {
1997                relativeCard = new JPanel();
1998            }
1999            if (absoluteCard == null) {
2000                absoluteCard = new JPanel();
2001            }
2002            timesCardPanelExtra.add(relativeCard, "relative");
2003            timesCardPanelExtra.add(absoluteCard, "absolute");
2004            timesPanel = GuiUtils.centerBottom(comps[0], timesCardPanelExtra);
2005        }
2006
2007        return timesPanel;
2008    }
2009
2010    private JPanel innerPanel = new JPanel();
2011
2012    private JLabel statusLabel = new JLabel("Status");
2013
2014    /**
2015     * Super setStatus() takes a second string to enable "simple" mode
2016     * which highlights the required component.  We don't really care
2017     * about that feature, and we don't want getStatusLabel() to
2018     * change the label background color.
2019     */
2020    @Override
2021    public void setStatus(String statusString, String foo) {
2022        if (statusString == null) {
2023            statusString = "";
2024        }
2025        statusLabel.setText(statusString);
2026    }
2027
2028    protected void setInnerPanel(JPanel newInnerPanel) {
2029        innerPanel = newInnerPanel;
2030    }
2031    
2032    /**
2033     * Create the widget responsible for handling relative time selection.
2034     *
2035     * @return GUI widget.
2036     */
2037    @Override public JComponent getRelativeTimesChooser() {
2038        McVTextField relativeTimesField =
2039            McVGuiUtils.makeTextFieldAllow(String.valueOf(relativeTimes),
2040                                           4,
2041                                           false,
2042                                           '0', '1', '2', '3', '4', '5', 
2043                                           '6', '7','8','9');
2044                                           
2045        // need to keep *both* the ActionListener and DocumentListener around.
2046        // removing the ActionListener results in strange behavior when you've
2047        // accidentally cleared out the text field.
2048        relativeTimesField.setAllow(Pattern.compile("^[1-9][0-9]*$"), true);
2049//        relativeTimesField.setDeny(Pattern.compile("^0$"), true);
2050        relativeTimesField.setColumns(4);
2051        relativeTimesField.addActionListener(e -> {
2052            String text = ((JTextField)e.getSource()).getText();
2053            validateRelativeTimeInput(text);
2054        });
2055        relativeTimesField.getDocument().addDocumentListener(new DocumentListener() {
2056            @Override public void insertUpdate(DocumentEvent e) {
2057                handleRelativeTimeChange(e);
2058            }
2059            
2060            @Override public void removeUpdate(DocumentEvent e) {
2061                handleRelativeTimeChange(e);
2062            }
2063            
2064            @Override public void changedUpdate(DocumentEvent e) {
2065                handleRelativeTimeChange(e);
2066            }
2067        });
2068        relativeTimesField.setToolTipText(RELATIVE_TIMES_TOOLTIP);
2069        
2070        JPanel panel =
2071            GuiUtils.topLeft(GuiUtils.label(RELATIVE_TIMES_LABEL,
2072                relativeTimesField));
2073        JScrollPane scrollPane = new JScrollPane(panel);
2074        scrollPane.setPreferredSize(new Dimension(150, 100));
2075        return scrollPane;
2076    }
2077    
2078    /**
2079     * Validate the contents of the relative times text field.
2080     * 
2081     * <p>This method overwrites {@link #relativeTimes} if {@code text} is an 
2082     * integer greater than zero.</p>
2083     * 
2084     * @param text Contents of the text field.
2085     */
2086    private void validateRelativeTimeInput(String text) {
2087        try {
2088            int value = Integer.valueOf(text);
2089            if (value > 0) {
2090                relativeTimes = value;
2091                setHaveData(true);
2092                setState(STATE_CONNECTED);
2093                updateStatus();
2094            }
2095        } catch (NumberFormatException e) {
2096            setState(STATUS_ERROR);
2097            setHaveData(false);
2098            setStatus("Please provide an integer value greater than zero.");
2099        }
2100    }
2101    
2102    /**
2103     * Handle {@link DocumentListener} events for the {@link JTextField}
2104     * created by {@link #getRelativeTimesChooser()}.
2105     *
2106     * @param event Event to handle. Cannot be {@code null}.
2107     */
2108    private void handleRelativeTimeChange(DocumentEvent event) {
2109        int len = event.getDocument().getLength();
2110        try {
2111            String text = event.getDocument().getText(0, len);
2112            validateRelativeTimeInput(text);
2113        } catch (BadLocationException ex) {
2114            logger.warn("Could not get contents of text field!", ex);
2115        }
2116    }
2117    
2118    /**
2119     * Get the relative time indices
2120     *
2121     * @return an array of indices
2122     */
2123    @Override public int[] getRelativeTimeIndices() {
2124        int[] indices = new int[relativeTimes];
2125        for (int i = 0; i < indices.length; i++) {
2126            indices[i] = i;
2127        }
2128        return indices;
2129    }
2130    
2131    /**
2132     * Make the UI for this selector.
2133     *
2134     * Thank you NetBeans for helping with the layout!
2135     *
2136     * @return The GUI.
2137     */
2138    protected JComponent doMakeContents() {
2139        JPanel outerPanel = new JPanel();
2140
2141        JLabel serverLabelInner = new JLabel("Server:");
2142        McVGuiUtils.setLabelPosition(serverLabelInner, Position.RIGHT);
2143        JPanel serverLabel = GuiUtils.leftRight(parameterButton, serverLabelInner);
2144        McVGuiUtils.setComponentWidth(serverLabel);
2145
2146        clearOnChange(serverSelector);
2147        McVGuiUtils.setComponentWidth(serverSelector, Width.TRIPLE);
2148        // McIDAS Inquiry #2146-3141 -> Changed from Width.DOUBLE to Width.TRIPLE
2149
2150        JLabel groupLabel = McVGuiUtils.makeLabelRight("Dataset:");
2151
2152        groupSelector.setEditable(isGroupEditable());
2153        clearOnChange(groupSelector);
2154        McVGuiUtils.setComponentWidth(groupSelector, Width.DOUBLE);
2155
2156        McVGuiUtils.setComponentWidth(connectButton, Width.DOUBLE);
2157        connectButton.setActionCommand(CMD_CONNECT);
2158        connectButton.addActionListener(this);
2159
2160        /* Set the attributes for the descriptor label and combo box, even though
2161         * they are not used here.  Extending classes can add them to the panel if
2162         * necessary.
2163         */
2164        McVGuiUtils.setComponentWidth(descriptorLabel);
2165        McVGuiUtils.setLabelPosition(descriptorLabel, Position.RIGHT);
2166
2167        // McIDAS Inquiry #2146-3141 -> Non-stanard length
2168        McVGuiUtils.setComponentWidth(descriptorComboBox, 584);
2169
2170        if (descriptorComboBox.getMinimumSize().getWidth() < ELEMENT_DOUBLE_WIDTH) {
2171            McVGuiUtils.setComponentWidth(descriptorComboBox, Width.DOUBLE);
2172        }
2173
2174        JLabel statusLabelLabel = McVGuiUtils.makeLabelRight("");
2175
2176        statusLabel.setText("Status");
2177        McVGuiUtils.setLabelPosition(statusLabel, Position.RIGHT);
2178        McVGuiUtils.setComponentColor(statusLabel, TextColor.STATUS);
2179
2180        JButton helpButton = McVGuiUtils.makeImageButton(ICON_HELP, "Show help");
2181        helpButton.setActionCommand(GuiUtils.CMD_HELP);
2182        helpButton.addActionListener(this);
2183
2184        JButton refreshButton = McVGuiUtils.makeImageButton(ICON_REFRESH, "Refresh");
2185        refreshButton.setActionCommand(GuiUtils.CMD_UPDATE);
2186        refreshButton.addActionListener(this);
2187
2188        McVGuiUtils.setComponentWidth(loadButton, Width.DOUBLE);
2189
2190        GroupLayout layout = new GroupLayout(outerPanel);
2191        outerPanel.setLayout(layout);
2192        layout.setHorizontalGroup(
2193            layout.createParallelGroup(LEADING)
2194            .addGroup(TRAILING, layout.createSequentialGroup()
2195                .addGroup(layout.createParallelGroup(TRAILING)
2196                    .addGroup(layout.createSequentialGroup()
2197                        .addContainerGap()
2198                        .addComponent(helpButton)
2199                        .addGap(GAP_RELATED)
2200                        .addComponent(refreshButton)
2201                        .addGap(GAP_RELATED)
2202                        .addComponent(cancelButton)
2203                        .addPreferredGap(RELATED)
2204                        .addComponent(loadButton))
2205                        .addGroup(LEADING, layout.createSequentialGroup()
2206                        .addContainerGap()
2207                        .addGroup(layout.createParallelGroup(LEADING)
2208                            .addComponent(innerPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
2209                            .addGroup(layout.createSequentialGroup()
2210                                .addComponent(serverLabel)
2211                                .addGap(GAP_RELATED)
2212                                .addComponent(serverSelector)
2213                                .addGap(GAP_RELATED)
2214                                .addComponent(manageButton)
2215                                .addGap(GAP_RELATED)
2216                                .addComponent(groupLabel)
2217                                .addGap(GAP_RELATED)
2218                                .addComponent(groupSelector)
2219                                .addGap(GAP_RELATED)
2220                                .addComponent(publicButton)
2221                                .addPreferredGap(RELATED, DEFAULT_SIZE, Short.MAX_VALUE)
2222                                .addComponent(connectButton))
2223                            .addGroup(layout.createSequentialGroup()
2224                                .addComponent(statusLabelLabel)
2225                                .addGap(GAP_RELATED)
2226                                .addComponent(statusLabel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)))))
2227                .addContainerGap())
2228        );
2229        layout.setVerticalGroup(
2230            layout.createParallelGroup(LEADING)
2231            .addGroup(layout.createSequentialGroup()
2232                .addContainerGap()
2233                .addGroup(layout.createParallelGroup(BASELINE)
2234                    .addComponent(serverLabel)
2235                    .addComponent(serverSelector)
2236                    .addComponent(manageButton)
2237                    .addComponent(groupLabel)
2238                    .addComponent(groupSelector)
2239                    .addComponent(publicButton)
2240                    .addComponent(connectButton))
2241                .addPreferredGap(UNRELATED)
2242                .addComponent(innerPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
2243                .addPreferredGap(UNRELATED)
2244                .addGroup(layout.createParallelGroup(BASELINE)
2245                    .addComponent(statusLabelLabel)
2246                    .addComponent(statusLabel))
2247                .addPreferredGap(UNRELATED)
2248                .addGroup(layout.createParallelGroup(BASELINE)
2249                    .addComponent(loadButton)
2250                    .addComponent(cancelButton)
2251                    .addComponent(refreshButton)
2252                    .addComponent(helpButton))
2253                .addContainerGap())
2254        );
2255    
2256        return outerPanel;
2257
2258    }
2259
2260    public class ServerComparator implements Comparator<AddeServer> {
2261        public int compare(AddeServer server1, AddeServer server2) {
2262            return server1.getName().compareTo(server2.getName());
2263        }
2264    }
2265
2266    public class GroupComparator implements Comparator<Group> {
2267        public int compare(Group group1, Group group2) {
2268            return group1.getName().compareTo(group2.getName());
2269        }
2270    }
2271}
2272