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