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