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