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 */
028package edu.wisc.ssec.mcidasv.servermanager;
029
030import static java.util.Objects.requireNonNull;
031
032import static javax.swing.GroupLayout.DEFAULT_SIZE;
033import static javax.swing.GroupLayout.PREFERRED_SIZE;
034import static javax.swing.GroupLayout.Alignment.BASELINE;
035import static javax.swing.GroupLayout.Alignment.LEADING;
036import static javax.swing.GroupLayout.Alignment.TRAILING;
037import static javax.swing.LayoutStyle.ComponentPlacement.RELATED;
038import static javax.swing.LayoutStyle.ComponentPlacement.UNRELATED;
039
040import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashSet;
041import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newMap;
042import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.set;
043import static edu.wisc.ssec.mcidasv.util.McVGuiUtils.runOnEDT;
044import static edu.wisc.ssec.mcidasv.util.McVGuiUtils.safeGetText;
045
046import java.awt.Color;
047import java.awt.Frame;
048import java.awt.event.ActionEvent;
049import java.awt.event.ActionListener;
050import java.awt.event.WindowEvent;
051import java.util.Collection;
052import java.util.Collections;
053import java.util.EnumSet;
054import java.util.LinkedHashSet;
055import java.util.LinkedHashMap;
056import java.util.List;
057import java.util.Map;
058import java.util.Set;
059import java.util.StringTokenizer;
060import java.util.concurrent.Callable;
061import java.util.concurrent.CompletionService;
062import java.util.concurrent.ExecutionException;
063import java.util.concurrent.ExecutorCompletionService;
064import java.util.concurrent.ExecutorService;
065import java.util.concurrent.Executors;
066import java.util.concurrent.Future;
067import java.util.concurrent.TimeUnit;
068import java.util.stream.Collectors;
069
070import javax.swing.BorderFactory;
071import javax.swing.GroupLayout;
072import javax.swing.JButton;
073import javax.swing.JCheckBox;
074import javax.swing.JDialog;
075import javax.swing.JLabel;
076import javax.swing.JPanel;
077import javax.swing.JTextField;
078import javax.swing.SwingUtilities;
079import javax.swing.WindowConstants;
080import javax.swing.event.DocumentEvent;
081import javax.swing.event.DocumentListener;
082
083import org.slf4j.Logger;
084import org.slf4j.LoggerFactory;
085
086import ucar.unidata.util.LogUtil;
087
088import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EditorAction;
089import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource;
090import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
091import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity;
092import edu.wisc.ssec.mcidasv.util.CollectionHelpers;
093import edu.wisc.ssec.mcidasv.util.McVTextField;
094
095/**
096 * Simple dialog that allows the user to define or modify
097 * {@link RemoteAddeEntry RemoteAddeEntries}.
098 */
099@SuppressWarnings("serial")
100public class RemoteEntryEditor extends JDialog {
101
102    /** Logger object. */
103    private static final Logger logger =
104        LoggerFactory.getLogger(RemoteEntryEditor.class);
105
106    /** Possible entry verification states. */
107    public enum AddeStatus {
108        PREFLIGHT, BAD_SERVER, BAD_ACCOUNTING, NO_METADATA, OK, BAD_GROUP
109    }
110
111    /** Number of threads in the thread pool. */
112    private static final int POOL = 5;
113
114    /**
115     * Whether or not to input in the dataset, username, and project fields
116     * should be uppercased.
117     */
118    private static final String PREF_FORCE_CAPS = "mcv.servers.forcecaps";
119
120    /**
121     * Background {@link java.awt.Color Color} of an {@literal "invalid"}
122     * {@link JTextField}.
123     */
124    private static final Color ERROR_FIELD_COLOR = Color.PINK;
125
126    /**
127     * Text {@link java.awt.Color Color} of an {@literal "invalid"}
128     * {@link JTextField}.
129     * */
130    private static final Color ERROR_TEXT_COLOR = Color.WHITE;
131
132    /**
133     * Background {@link java.awt.Color Color} of a {@literal "valid"}
134     * {@link JTextField}.
135     * */
136    private static final Color NORMAL_FIELD_COLOR = Color.WHITE;
137
138    /**
139     * Text {@link java.awt.Color Color} of a {@literal "valid"}
140     * {@link JTextField}.
141     */
142    private static final Color NORMAL_TEXT_COLOR = Color.BLACK;
143
144    /**
145     * Contains any {@code JTextField}s that may be in an invalid
146     * (to McIDAS-V) state.
147     */
148    private final Set<JTextField> badFields = newLinkedHashSet(25);
149
150    /** Server manager GUI. Value may be {@code null}. */
151    private final TabbedAddeManager manager;
152    
153    /** Reference back to the server manager. */
154    private final EntryStore entryStore;
155
156    /**
157     * Allows for asynchronous verification of ADDE entries.
158     * May be {@code null}.
159     */
160    private ExecutorService exec;
161
162    /** Current contents of the editor. */
163    private final Set<RemoteAddeEntry> currentEntries = newLinkedHashSet();
164
165    /** The last dialog action performed by the user. */
166    private EditorAction editorAction = EditorAction.INVALID;
167
168    /**
169     * Initial contents of {@link #serverField}.
170     * Be aware that {@code null} is allowed.
171     */
172    private final String serverText;
173
174    /**
175     * Initial contents of {@link #datasetField}.
176     * Be aware that {@code null} is allowed.
177     */
178    private final String datasetText;
179
180    /** Whether or not the editor is prompting the user to adjust input. */
181    private boolean inErrorState = false;
182
183    // if we decide to restore error overlays for known "bad" values.
184//    private Set<RemoteAddeEntry> invalidEntries = CollectionHelpers.newLinkedHashSet();
185
186    /**
187     * Populates the server and dataset text fields with given {@link String}s.
188     * This only works if the dialog <b>is not yet visible</b>.
189     * 
190     * <p>This is mostly useful when adding an entry from a chooser.
191     *
192     * @param entryStore Reference to the server manager.
193     * @param address Should be the address of a server, but empty and 
194     * {@code null} values are allowed.
195     * @param group Should be the name of a group/dataset on {@code server}, 
196     * but empty and {@code null} values are allowed.
197     */
198    public RemoteEntryEditor(EntryStore entryStore, String address, String group) {
199        super((JDialog)null, true);
200        this.entryStore = entryStore;
201        this.manager = null;
202        this.serverText = address;
203        this.datasetText = group;
204        initComponents(RemoteAddeEntry.INVALID_ENTRIES);
205    }
206
207    // TODO(jon): hold back on javadocs, this is likely to change
208    public RemoteEntryEditor(Frame parent, boolean modal, final TabbedAddeManager manager, final EntryStore store) {
209        this(parent, modal, manager, store, RemoteAddeEntry.INVALID_ENTRIES);
210    }
211
212    public RemoteEntryEditor(Frame parent, boolean modal, final TabbedAddeManager manager, final EntryStore store, final RemoteAddeEntry entry) {
213        this(parent, modal, manager, store, CollectionHelpers.list(entry));
214    }
215
216    // TODO(jon): hold back on javadocs, this is likely to change
217    public RemoteEntryEditor(Frame parent, boolean modal, final TabbedAddeManager manager, final EntryStore store, final List<RemoteAddeEntry> entries) {
218        super(manager, modal);
219        this.entryStore = store;
220        this.manager = manager;
221        this.serverText = null;
222        this.datasetText = null;
223        if (! entries.equals(RemoteAddeEntry.INVALID_ENTRIES)) {
224            currentEntries.addAll(entries);
225        }
226        initComponents(entries);
227    }
228
229    /**
230     * Poll the various UI components and attempt to construct valid ADDE
231     * entries based upon the information provided by the user.
232     *
233     * @param ignoreCheckboxes Whether or not the {@literal "type"} checkboxes
234     * should get ignored. Setting this to {@code true} means that <i>all</i>
235     * types are considered valid--which is useful when attempting to verify
236     * the user's input.
237     *
238     * @return {@link Set} of entries that represent the user's input, or an
239     * empty {@code Set} if the input was invalid somehow.
240     */
241    private Set<RemoteAddeEntry> pollWidgets(final boolean ignoreCheckboxes) {
242        String host = safeGetText(serverField).trim();
243        String dataset = safeGetText(datasetField).trim();
244        String username = RemoteAddeEntry.DEFAULT_ACCOUNT.getUsername();
245        String project = RemoteAddeEntry.DEFAULT_ACCOUNT.getProject();
246        if (acctBox.isSelected()) {
247            username = safeGetText(userField).trim();
248            project = safeGetText(projField).trim();
249        }
250
251        // determine the "valid" types
252        Set<EntryType> selectedTypes = newLinkedHashSet();
253        if (!ignoreCheckboxes) {
254            if (imageBox.isSelected()) {
255                selectedTypes.add(EntryType.IMAGE);
256            }
257            if (pointBox.isSelected()) {
258                selectedTypes.add(EntryType.POINT);
259            }
260            if (gridBox.isSelected()) {
261                selectedTypes.add(EntryType.GRID);
262            }
263            if (textBox.isSelected()) {
264                selectedTypes.add(EntryType.TEXT);
265            }
266            if (navBox.isSelected()) {
267                selectedTypes.add(EntryType.NAV);
268            }
269            if (radarBox.isSelected()) {
270                selectedTypes.add(EntryType.RADAR);
271            }
272        } else {
273            selectedTypes.addAll(set(EntryType.IMAGE, EntryType.POINT, EntryType.GRID, EntryType.TEXT, EntryType.NAV, EntryType.RADAR));
274        }
275
276        if (selectedTypes.isEmpty()) {
277            selectedTypes.add(EntryType.UNKNOWN);
278        }
279
280        // deal with the user trying to add multiple groups at once (even though this UI doesn't work right with it)
281        StringTokenizer tok = new StringTokenizer(dataset, ",");
282        Set<String> newDatasets = newLinkedHashSet();
283        while (tok.hasMoreTokens()) {
284            newDatasets.add(tok.nextToken().trim());
285        }
286
287        // create a new entry for each group and its valid types.
288        Set<RemoteAddeEntry> entries = newLinkedHashSet();
289        for (String newGroup : newDatasets) {
290            for (EntryType type : selectedTypes) {
291                RemoteAddeEntry.Builder builder = new RemoteAddeEntry.Builder(host, newGroup).type(type).validity(EntryValidity.VERIFIED).source(EntrySource.USER);
292                if (acctBox.isSelected()) {
293                    builder = builder.account(username, project);
294                }
295                RemoteAddeEntry newEntry = builder.build();
296                List<AddeEntry> matches = entryStore.searchWithPrefix(newEntry.asStringId());
297                if (matches.isEmpty()) {
298                    entries.add(newEntry);
299                } else if (matches.size() == 1) {
300                    AddeEntry matchedEntry = matches.get(0);
301                    if (matchedEntry.getEntrySource() != EntrySource.SYSTEM) {
302                        entries.add(newEntry);
303                    } else {
304                        entries.add((RemoteAddeEntry)matchedEntry);
305                    }
306                } else {
307                    // results should only be empty or a single entry
308                    logger.warn("server manager returned unexpected results={}", matches);
309                }
310            }
311        }
312        return entries;
313    }
314
315    private void disposeDisplayable(final boolean refreshManager) {
316        if (isDisplayable()) {
317            dispose();
318        }
319        TabbedAddeManager tmpController = TabbedAddeManager.getTabbedManager();
320        if (refreshManager && (tmpController != null)) {
321            tmpController.refreshDisplay();
322        }
323    }
324
325    /**
326     * Creates new {@link RemoteAddeEntry}s based upon the contents of the dialog
327     * and adds {@literal "them"} to the managed servers. If the dialog is
328     * displayed, we call {@link #dispose()} and attempt to refresh the
329     * server manager GUI if it is available.
330     */
331    private void addEntry() {
332        Set<RemoteAddeEntry> addedEntries = pollWidgets(false);
333        entryStore.addEntries(addedEntries);
334//        if (manager != null) {
335//            manager.addEntries(addedEntries);
336//        }
337        disposeDisplayable(true);
338    }
339
340    /**
341     * Replaces the entries within {@link #currentEntries} with new entries 
342     * from {@link #pollWidgets(boolean)}. If the dialog is displayed, we call 
343     * {@link #dispose()} and attempt to refresh the server manager GUI if it's 
344     * available.
345     */
346    private void editEntry() {
347        Set<RemoteAddeEntry> newEntries = pollWidgets(false);
348        entryStore.replaceEntries(currentEntries, newEntries);
349//        if (manager != null) {
350//            manager.replaceEntries(currentEntries, newEntries);
351//        }
352        logger.trace("currentEntries={}", currentEntries);
353        disposeDisplayable(true);
354    }
355
356    /**
357     * Attempts to verify that the current contents of the GUI are
358     * {@literal "valid"}.
359     */
360    private void verifyInput(final EditorAction action) {
361        resetBadFields();
362        Set<RemoteAddeEntry> unverifiedEntries = pollWidgets(true);
363
364        // the editor GUI only works with one server address at a time. so 
365        // although there may be several RemoteAddeEntry objs, they'll all have
366        // the same address and the following *isn't* as dumb as it looks!
367        if (!unverifiedEntries.isEmpty()) {
368            if (!RemoteAddeEntry.checkHost(unverifiedEntries.toArray(new RemoteAddeEntry[0])[0])) {
369                setStatus("Could not connect to the given server.");
370                setBadField(serverField, true);
371                return;
372            }
373        } else {
374            setStatus("Please specify ");
375            setBadField(serverField, true);
376            return;
377        }
378
379        setStatus("Contacting server...");
380        Thread checkThread = makeCheckThread(action, unverifiedEntries);
381        checkThread.start();
382    }
383
384    /**
385     * Displays a short status message in {@link #statusLabel}.
386     *
387     * @param msg Status message. Shouldn't be {@code null}.
388     */
389    private void setStatus(final String msg) {
390        assert msg != null;
391        logger.debug("msg={}", msg);
392        runOnEDT(() -> statusLabel.setText(msg));
393        statusLabel.revalidate();
394    }
395
396    /**
397     * Marks a {@code JTextField} as {@literal "valid"} or {@literal "invalid"}.
398     * Mostly this just means that the field is highlighted in order to provide
399     * to the user a sense of {@literal "what do I fix"} when something goes
400     * wrong.
401     *
402     * @param field {@code JTextField} to mark.
403     * @param isBad {@code true} means that the field is {@literal "invalid"},
404     * {@code false} means that the field is {@literal "valid"}.
405     */
406    private void setBadField(final JTextField field, final boolean isBad) {
407        assert field != null;
408        assert field == serverField || field == datasetField || field == userField || field == projField;
409
410        if (isBad) {
411            badFields.add(field);
412        } else {
413            badFields.remove(field);
414        }
415
416        runOnEDT(() -> {
417            if (isBad) {
418                field.setForeground(ERROR_TEXT_COLOR);
419                field.setBackground(ERROR_FIELD_COLOR);
420            } else {
421                field.setForeground(NORMAL_TEXT_COLOR);
422                field.setBackground(NORMAL_FIELD_COLOR);
423            }
424        });
425        field.revalidate();
426    }
427
428    /**
429     * Determines whether or not any fields are in an invalid state. Useful
430     * for disallowing the user to add invalid entries to the server manager.
431     *
432     * @return Whether or not any fields are invalid.
433     */
434    private boolean anyBadFields() {
435        assert badFields != null;
436        return !badFields.isEmpty();
437    }
438
439    /**
440     * Clear out {@link #badFields} and {@literal "set"} the field's status to
441     * valid.
442     */
443    private void resetBadFields() {
444        Set<JTextField> fields = new LinkedHashSet<>(badFields);
445        for (JTextField field : fields) {
446            setBadField(field, false);
447        }
448    }
449
450    /**
451     * Returns the last {@link EditorAction} that was performed.
452     *
453     * @return Last editor action performed.
454     *
455     * @see #editorAction
456     */
457    public EditorAction getEditorAction() {
458        return editorAction;
459    }
460
461    /**
462     * Set the {@link EditorAction} that was performed.
463     *
464     * @param editorAction Action that was performed.
465     *
466     * @see #editorAction
467     */
468    private void setEditorAction(final EditorAction editorAction) {
469        this.editorAction = editorAction;
470    }
471
472    /**
473     * Controls the value associated with the {@link #PREF_FORCE_CAPS} preference.
474     * 
475     * @param value {@code true} causes user input into the dataset, username, 
476     * and project fields to be capitalized.
477     * 
478     * @see #getForceMcxCaps()
479     */
480    private void setForceMcxCaps(final boolean value) {
481        entryStore.getIdvStore().put(PREF_FORCE_CAPS, value);
482    }
483
484    /**
485     * Returns the value associated with the {@link #PREF_FORCE_CAPS} preference.
486     *
487     * @return Whether or not user input should be automatically capitalized.
488     *
489     * @see #setForceMcxCaps(boolean)
490     */
491    private boolean getForceMcxCaps() {
492        return entryStore.getIdvStore().get(PREF_FORCE_CAPS, true);
493    }
494
495    // TODO(jon): oh man clean this junk up
496    /** This method is called from within the constructor to
497     * initialize the form.
498     * WARNING: Do NOT modify this code. The content of this method is
499     * always regenerated by the Form Editor.
500     *
501     * @param initEntries Inital remote ADDE entries to edit.
502     */
503    @SuppressWarnings("unchecked")
504    // <editor-fold defaultstate="collapsed" desc="Generated Code">
505    private void initComponents(final List<RemoteAddeEntry> initEntries) {
506        assert SwingUtilities.isEventDispatchThread();
507        entryPanel = new JPanel();
508        serverLabel = new JLabel();
509        serverField = new JTextField();
510        datasetLabel = new JLabel();
511        datasetField = new McVTextField();
512        acctBox = new JCheckBox();
513        userLabel = new JLabel();
514        userField = new McVTextField();
515        projLabel = new JLabel();
516        projField = new JTextField();
517        capBox = new JCheckBox();
518        typePanel = new JPanel();
519        imageBox = new JCheckBox();
520        pointBox = new JCheckBox();
521        gridBox = new JCheckBox();
522        textBox = new JCheckBox();
523        navBox = new JCheckBox();
524        radarBox = new JCheckBox();
525        statusPanel = new JPanel();
526        statusLabel = new JLabel();
527        verifyAddButton = new JButton();
528        verifyServer = new JButton();
529        addServer = new JButton();
530        cancelButton = new JButton();
531
532        boolean forceCaps = getForceMcxCaps();
533        datasetField.setUppercase(forceCaps);
534        userField.setUppercase(forceCaps);
535
536        if (initEntries == RemoteAddeEntry.INVALID_ENTRIES) {
537            setTitle("Add Remote Dataset");
538        } else {
539            setTitle("Edit Remote Dataset");
540        }
541        setResizable(false);
542        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
543        addWindowListener(new java.awt.event.WindowAdapter() {
544            public void windowClosed(WindowEvent evt) {
545                formWindowClosed(evt);
546            }
547        });
548
549        serverLabel.setText("Server:");
550        if (serverText != null) {
551            serverField.setText(serverText);
552        }
553
554        datasetLabel.setText("Dataset:");
555        if (datasetText != null) {
556            datasetField.setText(datasetText);
557        }
558
559        acctBox.setText("Specify accounting information:");
560        acctBox.addActionListener(this::acctBoxActionPerformed);
561
562        userLabel.setText("Username:");
563        userField.setEnabled(acctBox.isSelected());
564
565        projLabel.setText("Project #:");
566        projField.setEnabled(acctBox.isSelected());
567
568        capBox.setText("Automatically capitalize dataset and username?");
569        capBox.setSelected(forceCaps);
570        capBox.addActionListener(this::capBoxActionPerformed);
571
572        DocumentListener inputListener = new DocumentListener() {
573            public void changedUpdate(DocumentEvent evt) {
574                reactToValueChanges();
575            }
576            public void insertUpdate(DocumentEvent evt) {
577                if (inErrorState) {
578                    verifyAddButton.setEnabled(true);
579                    verifyServer.setEnabled(true);
580                    inErrorState = false;
581                    resetBadFields();
582                }
583            }
584            public void removeUpdate(DocumentEvent evt) {
585                if (inErrorState) {
586                    verifyAddButton.setEnabled(true);
587                    verifyServer.setEnabled(true);
588                    inErrorState = false;
589                    resetBadFields();
590                }
591            }
592        };
593
594        serverField.getDocument().addDocumentListener(inputListener);
595        datasetField.getDocument().addDocumentListener(inputListener);
596        userField.getDocument().addDocumentListener(inputListener);
597        projField.getDocument().addDocumentListener(inputListener);
598
599        GroupLayout entryPanelLayout = new GroupLayout(entryPanel);
600        entryPanel.setLayout(entryPanelLayout);
601        entryPanelLayout.setHorizontalGroup(
602            entryPanelLayout.createParallelGroup(LEADING)
603            .addGroup(entryPanelLayout.createSequentialGroup()
604                .addGroup(entryPanelLayout.createParallelGroup(LEADING)
605                    .addComponent(serverLabel, TRAILING)
606                    .addComponent(datasetLabel, TRAILING)
607                    .addComponent(userLabel, TRAILING)
608                    .addComponent(projLabel, TRAILING))
609                .addPreferredGap(RELATED)
610                .addGroup(entryPanelLayout.createParallelGroup(LEADING)
611                    .addComponent(serverField, DEFAULT_SIZE, 419, Short.MAX_VALUE)
612                    .addComponent(capBox)
613                    .addComponent(acctBox)
614                    .addComponent(datasetField, DEFAULT_SIZE, 419, Short.MAX_VALUE)
615                    .addComponent(userField, DEFAULT_SIZE, 419, Short.MAX_VALUE)
616                    .addComponent(projField, DEFAULT_SIZE, 419, Short.MAX_VALUE))
617                .addContainerGap())
618        );
619        entryPanelLayout.setVerticalGroup(
620            entryPanelLayout.createParallelGroup(LEADING)
621            .addGroup(entryPanelLayout.createSequentialGroup()
622                .addGroup(entryPanelLayout.createParallelGroup(BASELINE)
623                    .addComponent(serverLabel)
624                    .addComponent(serverField, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE))
625                .addPreferredGap(RELATED)
626                .addGroup(entryPanelLayout.createParallelGroup(BASELINE)
627                    .addComponent(datasetLabel)
628                    .addComponent(datasetField, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE))
629                .addGap(16, 16, 16)
630                .addComponent(acctBox)
631                .addPreferredGap(RELATED)
632                .addGroup(entryPanelLayout.createParallelGroup(BASELINE)
633                    .addComponent(userLabel)
634                    .addComponent(userField, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE))
635                .addPreferredGap(RELATED)
636                .addGroup(entryPanelLayout.createParallelGroup(BASELINE)
637                    .addComponent(projLabel)
638                    .addComponent(projField, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE))
639                .addPreferredGap(RELATED)
640                .addComponent(capBox)
641                .addGap(0, 0, Short.MAX_VALUE))
642        );
643
644        typePanel.setBorder(BorderFactory.createTitledBorder("Dataset Types"));
645
646        ActionListener typeInputListener = evt -> {
647            if (inErrorState) {
648                verifyAddButton.setEnabled(true);
649                verifyServer.setEnabled(true);
650                inErrorState = false;
651                resetBadFields();
652            }
653        };
654
655        imageBox.setText("Image");
656        imageBox.addActionListener(typeInputListener);
657        typePanel.add(imageBox);
658
659        pointBox.setText("Point");
660        pointBox.addActionListener(typeInputListener);
661        typePanel.add(pointBox);
662
663        gridBox.setText("Grid");
664        gridBox.addActionListener(typeInputListener);
665        typePanel.add(gridBox);
666
667        textBox.setText("Text");
668        textBox.addActionListener(typeInputListener);
669        typePanel.add(textBox);
670
671        navBox.setText("Navigation");
672        navBox.addActionListener(typeInputListener);
673        typePanel.add(navBox);
674
675        radarBox.setText("Radar");
676        radarBox.addActionListener(typeInputListener);
677        typePanel.add(radarBox);
678
679        statusPanel.setBorder(BorderFactory.createTitledBorder("Status"));
680
681        statusLabel.setText("Please provide the address of a remote ADDE server.");
682
683        GroupLayout statusPanelLayout = new GroupLayout(statusPanel);
684        statusPanel.setLayout(statusPanelLayout);
685        statusPanelLayout.setHorizontalGroup(
686            statusPanelLayout.createParallelGroup(LEADING)
687            .addGroup(statusPanelLayout.createSequentialGroup()
688                .addContainerGap()
689                .addComponent(statusLabel)
690                .addContainerGap(154, Short.MAX_VALUE))
691        );
692        statusPanelLayout.setVerticalGroup(
693            statusPanelLayout.createParallelGroup(LEADING)
694            .addGroup(statusPanelLayout.createSequentialGroup()
695                .addComponent(statusLabel)
696                .addContainerGap(DEFAULT_SIZE, Short.MAX_VALUE))
697        );
698
699        if (initEntries.equals(RemoteAddeEntry.INVALID_ENTRIES)) {
700            verifyAddButton.setText("Verify and Add Server");
701        } else {
702            verifyAddButton.setText("Verify and Save Changes");
703        }
704        verifyAddButton.addActionListener(evt -> {
705            if (initEntries == RemoteAddeEntry.INVALID_ENTRIES)
706                verifyAddButtonActionPerformed(evt);
707            else
708                verifyEditButtonActionPerformed(evt);
709        });
710
711        if (initEntries == RemoteAddeEntry.INVALID_ENTRIES) {
712            verifyServer.setText("Verify Server");
713        } else {
714            verifyServer.setText("Verify Changes");
715        }
716        verifyServer.addActionListener(evt -> verifyServerActionPerformed(evt));
717
718        if (initEntries.equals(RemoteAddeEntry.INVALID_ENTRIES)) {
719            addServer.setText("Add Server");
720        } else {
721            addServer.setText("Save Changes");
722        }
723        addServer.addActionListener(evt -> {
724            if (initEntries == RemoteAddeEntry.INVALID_ENTRIES) {
725                addServerActionPerformed(evt);
726            } else {
727                editServerActionPerformed(evt);
728            }
729        });
730
731        cancelButton.setText("Cancel");
732        cancelButton.addActionListener(this::cancelButtonActionPerformed);
733
734        GroupLayout layout = new GroupLayout(getContentPane());
735        getContentPane().setLayout(layout);
736        layout.setHorizontalGroup(
737            layout.createParallelGroup(LEADING)
738            .addGroup(layout.createSequentialGroup()
739                .addContainerGap()
740                .addGroup(layout.createParallelGroup(LEADING)
741                    .addComponent(statusPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
742                    .addComponent(typePanel, 0, 0, Short.MAX_VALUE)
743                    .addComponent(entryPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
744                    .addGroup(layout.createSequentialGroup()
745                        .addComponent(verifyAddButton)
746                        .addPreferredGap(RELATED)
747                        .addComponent(verifyServer)
748                        .addPreferredGap(RELATED)
749                        .addComponent(addServer)
750                        .addPreferredGap(RELATED)
751                        .addComponent(cancelButton)))
752                .addContainerGap())
753        );
754        layout.setVerticalGroup(
755            layout.createParallelGroup(LEADING)
756            .addGroup(layout.createSequentialGroup()
757                .addContainerGap()
758                .addComponent(entryPanel, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE)
759                .addPreferredGap(UNRELATED)
760                .addComponent(typePanel, PREFERRED_SIZE, 57, PREFERRED_SIZE)
761                .addGap(18, 18, 18)
762                .addComponent(statusPanel, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE)
763                .addGap(18, 18, 18)
764                .addGroup(layout.createParallelGroup(BASELINE)
765                    .addComponent(verifyServer)
766                    .addComponent(addServer)
767                    .addComponent(cancelButton)
768                    .addComponent(verifyAddButton))
769                .addContainerGap(17, Short.MAX_VALUE))
770        );
771
772        if ((initEntries != null) && !RemoteAddeEntry.INVALID_ENTRIES.equals(initEntries)) {
773            RemoteAddeEntry initEntry = initEntries.get(0);
774            boolean hasSystemEntry = false;
775            for (RemoteAddeEntry entry : initEntries) {
776                if (entry.getEntrySource() == EntrySource.SYSTEM) {
777                    initEntry = entry;
778                    hasSystemEntry = true;
779                    break;
780                }
781            }
782            serverField.setText(initEntry.getAddress());
783            datasetField.setText(initEntry.getGroup());
784
785            if (!RemoteAddeEntry.DEFAULT_ACCOUNT.equals(initEntry.getAccount())) {
786                acctBox.setSelected(true);
787                userField.setEnabled(true);
788                userField.setText(initEntry.getAccount().getUsername());
789                projField.setEnabled(true);
790                projField.setText(initEntry.getAccount().getProject());
791            }
792
793            if (hasSystemEntry) {
794                serverField.setEnabled(false);
795                datasetField.setEnabled(false);
796                acctBox.setEnabled(false);
797                userField.setEnabled(false);
798                projField.setEnabled(false);
799                capBox.setEnabled(false);
800            }
801
802            for (RemoteAddeEntry entry : initEntries) {
803                boolean nonDefaultSource = entry.getEntrySource() != EntrySource.SYSTEM;
804                if (entry.getEntryType() == EntryType.IMAGE) {
805                    imageBox.setSelected(true);
806                    imageBox.setEnabled(nonDefaultSource);
807                } else if (entry.getEntryType() == EntryType.POINT) {
808                    pointBox.setSelected(true);
809                    pointBox.setEnabled(nonDefaultSource);
810                } else if (entry.getEntryType() == EntryType.GRID) {
811                    gridBox.setSelected(true);
812                    gridBox.setEnabled(nonDefaultSource);
813                } else if (entry.getEntryType() == EntryType.TEXT) {
814                    textBox.setSelected(true);
815                    textBox.setEnabled(nonDefaultSource);
816                } else if (entry.getEntryType() == EntryType.NAV) {
817                    navBox.setSelected(true);
818                    navBox.setEnabled(nonDefaultSource);
819                } else if (entry.getEntryType() == EntryType.RADAR) {
820                    radarBox.setSelected(true);
821                    radarBox.setEnabled(nonDefaultSource);
822                }
823            }
824        }
825        pack();
826        setLocationRelativeTo(manager);
827    }// </editor-fold>
828
829    private void acctBoxActionPerformed(ActionEvent evt) {
830        assert SwingUtilities.isEventDispatchThread();
831        resetBadFields();
832        boolean enabled = acctBox.isSelected();
833        userField.setEnabled(enabled);
834        projField.setEnabled(enabled);
835        verifyAddButton.setEnabled(true);
836        verifyServer.setEnabled(true);
837    }
838
839    private void capBoxActionPerformed(ActionEvent evt) {
840        assert SwingUtilities.isEventDispatchThread();
841        boolean forceCaps = capBox.isSelected();
842        datasetField.setUppercase(forceCaps);
843        userField.setUppercase(forceCaps);
844        setForceMcxCaps(forceCaps);
845        if (!forceCaps) {
846            return;
847        }
848        datasetField.setText(safeGetText(datasetField).toUpperCase());
849        userField.setText(safeGetText(userField).toUpperCase());
850    }
851
852    private void verifyAddButtonActionPerformed(ActionEvent evt) {
853        verifyInput(EditorAction.VERIFYING_AND_ADDING);
854    }
855
856    private void handleVerifyAdd() {
857        if (!anyBadFields()) {
858            setEditorAction(EditorAction.ADDED_VERIFIED);
859            addEntry();
860        } else {
861            inErrorState = true;
862            verifyAddButton.setEnabled(false);
863            verifyServer.setEnabled(false);
864        }
865    }
866
867    private void verifyEditButtonActionPerformed(ActionEvent evt) {
868        verifyInput(EditorAction.VERIFYING_AND_EDITING);
869    }
870
871    private void handleVerifyEdit() {
872        if (!anyBadFields()) {
873            setEditorAction(EditorAction.EDITED_VERIFIED);
874            editEntry();
875        } else {
876            inErrorState = true;
877            verifyAddButton.setEnabled(false);
878            verifyServer.setEnabled(false);
879        }
880    }
881
882    private void cancelButtonActionPerformed(ActionEvent evt) {
883        setEditorAction(EditorAction.CANCELLED);
884        disposeDisplayable(false);
885        Thread t = new Thread() {
886            @Override public void run() {
887                if (exec != null) {
888                    exec.shutdownNow();
889                }
890            }
891        };
892        t.start();
893    }
894
895    private void formWindowClosed(WindowEvent evt) {
896        setEditorAction(EditorAction.CANCELLED);
897        disposeDisplayable(false);
898    }
899
900    private void verifyServerActionPerformed(ActionEvent evt) {
901        verifyInput(EditorAction.VERIFYING);
902        if (anyBadFields()) {
903            // save poll widget state
904            // toggle a "listen for *any* input event" switch to on
905//            invalidEntries.clear();
906//            invalidEntries.addAll(pollWidgets(false));
907            inErrorState = true;
908            verifyAddButton.setEnabled(false);
909            verifyServer.setEnabled(false);
910        }
911    }
912
913    private void addServerActionPerformed(ActionEvent evt) {
914        setEditorAction(EditorAction.ADDED);
915        addEntry();
916    }
917
918    private void editServerActionPerformed(ActionEvent evt) {
919        setEditorAction(EditorAction.EDITED);
920        editEntry();
921    }
922
923    private void reactToValueChanges() {
924        assert SwingUtilities.isEventDispatchThread();
925        if (inErrorState) {
926            verifyAddButton.setEnabled(true);
927            verifyServer.setEnabled(true);
928            inErrorState = false;
929            resetBadFields();
930        }
931    }
932
933    /**
934     * Attempt to verify a {@link Set} of {@link RemoteAddeEntry}s. Useful for
935     * checking a {@literal "MCTABLE.TXT"} after importing.
936     * 
937     * @param entries {@code Set} of remote ADDE entries to validate. Cannot 
938     * be {@code null}.
939     * 
940     * @return {@code Set} of {@code RemoteAddeEntry}s that McIDAS-V was able
941     * to connect to. 
942     * 
943     * @throws NullPointerException if {@code entries} is {@code null}.
944     */
945    public Set<RemoteAddeEntry> checkHosts(final Set<RemoteAddeEntry> entries) {
946        requireNonNull(entries, "entries cannot be null");
947        Set<RemoteAddeEntry> goodEntries = newLinkedHashSet();
948        Set<String> checkedHosts = newLinkedHashSet();
949        Map<String, Boolean> hostStatus = newMap();
950        for (RemoteAddeEntry entry : entries) {
951            String host = entry.getAddress();
952            if (hostStatus.get(host).equals(Boolean.TRUE)) {
953                goodEntries.add(entry);
954            } else {
955                checkedHosts.add(host);
956                if (RemoteAddeEntry.checkHost(entry)) {
957                    goodEntries.add(entry);
958                    hostStatus.put(host, Boolean.TRUE);
959                } else {
960                    hostStatus.put(host, Boolean.FALSE);
961                }
962            }
963        }
964        return goodEntries;
965    }
966
967    private Thread makeCheckThread(final EditorAction action,
968                                   final Set<RemoteAddeEntry> entries)
969    {
970        return new Thread() {
971            @Override public void run() {
972                logger.trace("checking entries...");
973                checkGroups(action, entries);
974            }
975        };
976    }
977
978    private void setCheckBoxes(final Set<RemoteAddeEntry> verified) {
979        SwingUtilities.invokeLater(() -> {
980            EnumSet<EntryType> presentTypes =
981                    EnumSet.noneOf(EntryType.class);
982
983            presentTypes.addAll(
984                    verified.stream()
985                            .map(RemoteAddeEntry::getEntryType)
986                            .collect(Collectors.toList()));
987
988            imageBox.setSelected(presentTypes.contains(EntryType.IMAGE));
989            pointBox.setSelected(presentTypes.contains(EntryType.POINT));
990            gridBox.setSelected(presentTypes.contains(EntryType.GRID));
991            textBox.setSelected(presentTypes.contains(EntryType.TEXT));
992            navBox.setSelected(presentTypes.contains(EntryType.NAV));
993            radarBox.setSelected(presentTypes.contains(EntryType.RADAR));
994        });
995    }
996
997    public Set<RemoteAddeEntry> checkGroups(final EditorAction action,
998                                            final Set<RemoteAddeEntry> entries)
999    {
1000        requireNonNull(entries, "entries cannot be null");
1001        if (entries.isEmpty()) {
1002            return Collections.emptySet();
1003        }
1004
1005        exec = Executors.newFixedThreadPool(POOL);
1006
1007        Set<RemoteAddeEntry> verified = newLinkedHashSet(entries.size());
1008        Collection<AddeStatus> statuses = EnumSet.noneOf(AddeStatus.class);
1009
1010        CompletionService<StatusWrapper> ecs =
1011                new ExecutorCompletionService<>(exec);
1012
1013        Map<RemoteAddeEntry, AddeStatus> entry2Status =
1014                new LinkedHashMap<>(entries.size());
1015
1016        // submit new verification tasks to the pool's queue
1017        // ...
1018        // (apologies for the pun?)
1019        for (RemoteAddeEntry entry : entries) {
1020            StatusWrapper pairing = new StatusWrapper(entry);
1021            ecs.submit(new VerifyEntryTask(pairing));
1022        }
1023
1024        // use completion service magic to only deal with finished
1025        // verification tasks
1026        try {
1027            int checkedEntries = 0;
1028            while (checkedEntries != entries.size()) {
1029
1030                // determine if the user has cancelled their verification
1031                // request
1032                if ((exec != null) && exec.isShutdown()) {
1033                    break;
1034                }
1035
1036                Future<StatusWrapper> future =
1037                        ecs.poll(300, TimeUnit.MILLISECONDS);
1038
1039                if (future != null && future.isDone()) {
1040                    StatusWrapper pairing = future.get();
1041                    RemoteAddeEntry entry = pairing.getEntry();
1042                    AddeStatus status = pairing.getStatus();
1043                    setStatus(entry.getEntryText()+": attempting verification...");
1044                    statuses.add(status);
1045                    entry2Status.put(entry, status);
1046                    if (status == AddeStatus.OK) {
1047                        verified.add(entry);
1048                        setStatus("Found accessible "+entry.getEntryType().toString().toLowerCase()+" data.");
1049                    }
1050                    checkedEntries++;
1051                    setCheckBoxes(verified);
1052                }
1053            }
1054        } catch (InterruptedException e) {
1055            LogUtil.logException("interrupted while checking ADDE entries", e);
1056        } catch (ExecutionException e) {
1057            LogUtil.logException("ADDE validation execution error", e);
1058        } finally {
1059            exec.shutdown();
1060        }
1061
1062        if (!statuses.contains(AddeStatus.OK)) {
1063            if (statuses.contains(AddeStatus.BAD_ACCOUNTING)) {
1064                setStatus("Incorrect accounting information.");
1065                setBadField(userField, true);
1066                setBadField(projField, true);
1067            } else if (statuses.contains(AddeStatus.BAD_GROUP)) {
1068                setStatus("Dataset does not appear to be valid.");
1069                setBadField(datasetField, true);
1070            } else if (statuses.contains(AddeStatus.BAD_SERVER)) {
1071                setStatus("Could not connect to the ADDE server.");
1072                setBadField(serverField, true);
1073            } else {
1074                logger.debug("no statuses are available; user may have cancelled");
1075            }
1076        } else {
1077            setStatus("Finished verifying.");
1078        }
1079
1080        if (EditorAction.VERIFYING_AND_ADDING.equals(action)) {
1081            handleVerifyAdd();
1082        } else if (EditorAction.VERIFYING_AND_EDITING.equals(action)) {
1083            handleVerifyEdit();
1084        }
1085        return verified;
1086    }
1087
1088    private static Map<RemoteAddeEntry, AddeStatus> bulkPut(final Collection<RemoteAddeEntry> entries, final AddeStatus status) {
1089        Map<RemoteAddeEntry, AddeStatus> map = new LinkedHashMap<>(entries.size());
1090        for (RemoteAddeEntry entry : entries) {
1091            map.put(entry, status);
1092        }
1093        return map;
1094    }
1095
1096    /**
1097     * Associates a {@link RemoteAddeEntry} with one of the states from 
1098     * {@link AddeStatus}.
1099     */
1100    private static class StatusWrapper {
1101        /** */
1102        private final RemoteAddeEntry entry;
1103
1104        /** Current {@literal "status"} of {@link #entry}. */
1105        private AddeStatus status;
1106
1107        /**
1108         * Builds an entry/status pairing.
1109         * 
1110         * @param entry The {@code RemoteAddeEntry} to wrap up.
1111         * 
1112         * @throws NullPointerException if {@code entry} is {@code null}.
1113         */
1114        public StatusWrapper(final RemoteAddeEntry entry) {
1115            requireNonNull(entry, "cannot create a entry/status pair with a null descriptor");
1116            this.entry = entry;
1117        }
1118
1119        /**
1120         * Set the {@literal "status"} of this {@link #entry} to a given 
1121         * {@link AddeStatus}.
1122         * 
1123         * @param status New status of {@code entry}.
1124         */
1125        public void setStatus(AddeStatus status) {
1126            this.status = status;
1127        }
1128
1129        /**
1130         * Returns the current {@literal "status"} of {@link #entry}.
1131         * 
1132         * @return One of {@link AddeStatus}.
1133         */
1134        public AddeStatus getStatus() {
1135            return status;
1136        }
1137
1138        /**
1139         * Returns the {@link RemoteAddeEntry} stored in this wrapper.
1140         * 
1141         * @return {@link #entry}
1142         */
1143        public RemoteAddeEntry getEntry() {
1144            return entry;
1145        }
1146    }
1147
1148    /**
1149     * Represents an ADDE entry verification task. These are executed asynchronously 
1150     * by the completion service within
1151     * {@link RemoteEntryEditor#checkGroups(EditorAction, Set)}.
1152     */
1153    private class VerifyEntryTask implements Callable<StatusWrapper> {
1154        private final StatusWrapper entryStatus;
1155        public VerifyEntryTask(final StatusWrapper descStatus) {
1156            requireNonNull(descStatus, "cannot verify or set status of a null descriptor/status pair");
1157            this.entryStatus = descStatus;
1158        }
1159
1160        @Override public StatusWrapper call() throws Exception {
1161            entryStatus.setStatus(RemoteAddeEntry.checkEntry(entryStatus.getEntry()));
1162            return entryStatus;
1163        }
1164    }
1165    
1166    private class VerifyHostTask implements Callable<StatusWrapper> {
1167        private final StatusWrapper entryStatus;
1168        public VerifyHostTask(final StatusWrapper descStatus) {
1169            entryStatus = requireNonNull(descStatus, "cannot verify or set status of a null descriptor/status pair");
1170        }
1171        @Override public StatusWrapper call() throws Exception {
1172            boolean validHost = RemoteAddeEntry.checkHost(entryStatus.getEntry());
1173            if (validHost) {
1174                entryStatus.setStatus(AddeStatus.OK);
1175            } else {
1176                entryStatus.setStatus(AddeStatus.BAD_SERVER);
1177            }
1178            return entryStatus;
1179        }
1180    }
1181
1182    // Variables declaration - do not modify
1183    private JCheckBox acctBox;
1184    private JButton addServer;
1185    private JButton cancelButton;
1186    private JCheckBox capBox;
1187    private McVTextField datasetField;
1188    private JLabel datasetLabel;
1189    private JPanel entryPanel;
1190    private JCheckBox gridBox;
1191    private JCheckBox imageBox;
1192    private JCheckBox navBox;
1193    private JCheckBox pointBox;
1194    private JTextField projField;
1195    private JLabel projLabel;
1196    private JCheckBox radarBox;
1197    private JTextField serverField;
1198    private JLabel serverLabel;
1199    private JLabel statusLabel;
1200    private JPanel statusPanel;
1201    private JCheckBox textBox;
1202    private JPanel typePanel;
1203    private McVTextField userField;
1204    private JLabel userLabel;
1205    private JButton verifyAddButton;
1206    private JButton verifyServer;
1207    // End of variables declaration
1208}