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