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