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