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