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