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