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 edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
033import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashSet;
034import static edu.wisc.ssec.mcidasv.util.McVGuiUtils.runOnEDT;
035import static edu.wisc.ssec.mcidasv.util.McVGuiUtils.safeGetText;
036
037import java.awt.BorderLayout;
038import java.awt.Component;
039import java.awt.Dimension;
040import java.awt.Font;
041import java.awt.event.WindowAdapter;
042import java.awt.event.WindowEvent;
043import java.io.File;
044import java.util.Collection;
045import java.util.Collections;
046import java.util.EnumSet;
047import java.util.List;
048import java.util.Set;
049import java.util.concurrent.Callable;
050import java.util.concurrent.CompletionService;
051import java.util.concurrent.ExecutionException;
052import java.util.concurrent.ExecutorCompletionService;
053import java.util.concurrent.ExecutorService;
054import java.util.concurrent.Executors;
055import java.util.regex.Pattern;
056
057import javax.swing.Box;
058import javax.swing.BoxLayout;
059import javax.swing.GroupLayout;
060import javax.swing.Icon;
061import javax.swing.JButton;
062import javax.swing.JCheckBox;
063import javax.swing.JCheckBoxMenuItem;
064import javax.swing.JDialog;
065import javax.swing.JFileChooser;
066import javax.swing.JFrame;
067import javax.swing.JLabel;
068import javax.swing.JMenu;
069import javax.swing.JMenuBar;
070import javax.swing.JMenuItem;
071import javax.swing.JPanel;
072import javax.swing.JPopupMenu;
073import javax.swing.JScrollPane;
074import javax.swing.JSeparator;
075import javax.swing.JTabbedPane;
076import javax.swing.JTable;
077import javax.swing.JTextField;
078import javax.swing.LayoutStyle;
079import javax.swing.ListSelectionModel;
080import javax.swing.SwingUtilities;
081import javax.swing.UIManager;
082import javax.swing.WindowConstants;
083import javax.swing.border.EmptyBorder;
084import javax.swing.event.ChangeEvent;
085import javax.swing.event.ChangeListener;
086import javax.swing.event.ListSelectionEvent;
087import javax.swing.event.ListSelectionListener;
088import javax.swing.table.AbstractTableModel;
089import javax.swing.table.DefaultTableCellRenderer;
090
091import net.miginfocom.swing.MigLayout;
092
093import org.bushe.swing.event.EventBus;
094import org.bushe.swing.event.annotation.AnnotationProcessor;
095import org.bushe.swing.event.annotation.EventSubscriber;
096
097import org.slf4j.Logger;
098import org.slf4j.LoggerFactory;
099
100import ucar.unidata.idv.IdvObjectStore;
101import ucar.unidata.util.GuiUtils;
102import ucar.unidata.util.LogUtil;
103
104import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource;
105import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
106import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity;
107import edu.wisc.ssec.mcidasv.servermanager.AddeThread.McservEvent;
108import edu.wisc.ssec.mcidasv.servermanager.EntryStore.Event;
109import edu.wisc.ssec.mcidasv.servermanager.RemoteEntryEditor.AddeStatus;
110import edu.wisc.ssec.mcidasv.ui.BetterJTable;
111import edu.wisc.ssec.mcidasv.util.McVTextField.Prompt;
112import java.awt.event.ActionListener;
113import java.awt.event.ActionEvent;
114
115/**
116 * This class is the GUI frontend to {@link EntryStore} (the server manager).
117 * It allows users to manipulate their local and remote ADDE data.
118 */
119// TODO(jon): don't forget to persist tab choice and window position. maybe also the "positions" of the scrollpanes (if possible).
120// TODO(jon): GUI could look much better.
121// TODO(jon): finish up the javadocs.
122@SuppressWarnings({"serial", "AssignmentToStaticFieldFromInstanceMethod", "FieldCanBeLocal"})
123public class TabbedAddeManager extends JFrame {
124
125    /** Pretty typical logger object. */
126    private final static Logger logger = LoggerFactory.getLogger(TabbedAddeManager.class);
127
128    /** Path to the help resources. */
129    private static final String HELP_TOP_DIR = "/docs/userguide";
130
131    /** Help target for the remote servers. */
132    private static final String REMOTE_HELP_TARGET = "idv.tools.remotedata";
133
134    /** Help target for the local servers. */
135    private static final String LOCAL_HELP_TARGET = "idv.tools.localdata";
136
137    /** ID used to save/restore the last visible tab between sessions. */
138    private static final String LAST_TAB = "mcv.adde.lasttab";
139
140    /** ID used to save/restore the last directory that contained a MCTABLE.TXT. */
141    private static final String LAST_IMPORTED = "mcv.adde.lastmctabledir";
142
143    /** Size of the ADDE entry verification thread pool. */
144    private static final int POOL = 2;
145
146    /** Static reference to an instance of this class. Bad idea! */
147    private static TabbedAddeManager staticTabbedManager;
148
149    /**
150     * These are the various {@literal "events"} that the server manager GUI
151     * supports. These are published via the wonderful {@link EventBus#publish(Object)} method.
152     */
153    public enum Event { 
154        /** The GUI was created. */
155        OPENED,
156        /** The GUI was hidden/minimized/etc. */
157        HIDDEN,
158        /** GUI was unhidden or some such thing. */
159        SHOWN,
160        /** The GUI was closed. */
161        CLOSED
162    }
163
164    /** Reference to the actual server manager. */
165    private final EntryStore serverManager;
166
167    /** 
168     * Entries stored within the server manager GUI. This may differ from the
169     * contents of the server manager itself. 
170     */
171//    private final Set<AddeEntry> entrySet;
172
173    /** */
174    private final List<RemoteAddeEntry> selectedRemoteEntries;
175
176    /** */
177    private final List<LocalAddeEntry> selectedLocalEntries;
178
179    /** */
180    private JTextField importUser;
181
182    /** */
183    private JTextField importProject;
184
185    /** Whether or not {@link #initComponents()} has been called. */
186    private boolean guiInitialized = false;
187
188    /**
189     * Creates a standalone server manager GUI.
190     */
191    public TabbedAddeManager() {
192        //noinspection AssignmentToNull
193        AnnotationProcessor.process(this);
194        this.serverManager = null;
195//        this.entrySet = newLinkedHashSet();
196        this.selectedLocalEntries = arrList();
197        this.selectedRemoteEntries = arrList();
198
199        SwingUtilities.invokeLater(new Runnable() {
200            @Override public void run() {
201                initComponents();
202            }
203        });
204    }
205
206    /**
207     * Creates a server manager GUI that's linked back to the rest of McIDAS-V.
208     * 
209     * @param entryStore Server manager reference.
210     * 
211     * @throws NullPointerException if {@code entryStore} is {@code null}.
212     */
213    public TabbedAddeManager(final EntryStore entryStore) {
214        requireNonNull(entryStore, "Cannot pass a null server manager");
215        AnnotationProcessor.process(this);
216        this.serverManager = entryStore; 
217        this.selectedLocalEntries = arrList();
218        this.selectedRemoteEntries = arrList();
219        SwingUtilities.invokeLater(new Runnable() {
220            @Override public void run() {
221                initComponents();
222            }
223        });
224    }
225
226    /** 
227     * Returns an instance of this class. The instance <i>should</i> correspond
228     * to the one being used by the {@literal "rest"} of McIDAS-V.
229     * 
230     * @return Either an instance of this class or {@code null}.
231     */
232    public static TabbedAddeManager getTabbedManager() {
233        return staticTabbedManager;
234    }
235
236//    public void addEntries(final Collection<? extends AddeEntry> entries) {
237//        logger.trace("entries={}", entries);
238//        entrySet.addAll(entries);
239//        SwingUtilities.invokeLater(new Runnable() {
240//            public void run() {
241//                refreshDisplay();
242//            }
243//        });
244//    }
245//
246//    public void replaceEntries(final Collection<? extends AddeEntry> currentEntries, final Collection<? extends AddeEntry> newEntries) {
247//        logger.trace("currentEntries={} newEntries={}", currentEntries, newEntries);
248//        entrySet.removeAll(currentEntries);
249//        entrySet.addAll(newEntries);
250//        SwingUtilities.invokeLater(new Runnable() {
251//            public void run() {
252//                refreshDisplay();
253//            }
254//        });
255//    }
256//
257//    public void removeEntries(final Collection<? extends AddeEntry> entries) {
258//        logger.trace("entries={}", entries);
259//        entrySet.removeAll(entries);
260//        SwingUtilities.invokeLater(new Runnable() {
261//            public void run() {
262//                refreshDisplay();
263//            }
264//        });
265//    }
266
267    /**
268     * If the GUI isn't shown, this method will display things. If the GUI <i>is 
269     * shown</i>, bring it to the front.
270     * 
271     * <p>This method publishes {@link Event#SHOWN}.
272     */
273    public void showManager() {
274        if (!isVisible()) {
275            setVisible(true);
276        } else {
277            toFront();
278        }
279        staticTabbedManager = this;
280        EventBus.publish(Event.SHOWN);
281    }
282
283    /**
284     * Closes and disposes (if needed) the GUI.
285     */
286    public void closeManager() {
287        //noinspection AssignmentToNull
288        staticTabbedManager = null;
289        EventBus.publish(Event.CLOSED);
290        if (isDisplayable()) {
291            dispose();
292        }
293    }
294
295    /**
296     * Attempts to refresh the contents of both the local and remote dataset
297     * tables. 
298     */
299    public void refreshDisplay() {
300        if (guiInitialized) {
301            ((RemoteAddeTableModel)remoteTable.getModel()).refreshEntries();
302            ((LocalAddeTableModel)localTable.getModel()).refreshEntries();
303        }
304    }
305
306    /**
307     * Create and show the GUI the remote ADDE dataset GUI. Since no 
308     * {@link RemoteAddeEntry RemoteAddeEntries} have been provided, none of 
309     * the fields will be prefilled (user is creating a new dataset). 
310     */
311    // TODO(jon): differentiate between showRemoteEditor() and showRemoteEditor(entries)
312    public void showRemoteEditor() {
313        if (tabbedPane.getSelectedIndex() != 0) {
314            tabbedPane.setSelectedIndex(0);
315        }
316        RemoteEntryEditor editor = new RemoteEntryEditor(this, true, this, serverManager);
317        editor.setVisible(true);
318    }
319
320    /**
321     * Create and show the GUI the remote ADDE dataset GUI. Since some 
322     * {@link RemoteAddeEntry RemoteAddeEntries} have been provided, all of the
323     * applicable fields will be filled (user is editing an existing dataset).
324     * 
325     * @param entries Selection to edit. Should not be {@code null}.
326     */
327    // TODO(jon): differentiate between showRemoteEditor() and showRemoteEditor(entries)
328    public void showRemoteEditor(final List<RemoteAddeEntry> entries) {
329        if (tabbedPane.getSelectedIndex() != 0) {
330            tabbedPane.setSelectedIndex(0);
331        }
332        RemoteEntryEditor editor = new RemoteEntryEditor(this, true, this, serverManager, entries);
333        editor.setVisible(true);
334    }
335
336    /**
337     * Removes the given remote ADDE entries from the server manager GUI.
338     * 
339     * @param entries Entries to remove. {@code null} is permissible, but is a {@literal "no-op"}.
340     */
341    public void removeRemoteEntries(final Collection<RemoteAddeEntry> entries) {
342        if (entries == null) {
343            return;
344        }
345        List<RemoteAddeEntry> removable = arrList(entries.size());
346        for (RemoteAddeEntry entry : entries) {
347            if (entry.getEntrySource() != EntrySource.SYSTEM) {
348                removable.add(entry);
349            }
350        }
351//        if (entrySet.removeAll(removable)) {
352        if (serverManager.removeEntries(removable)) {
353            RemoteAddeTableModel tableModel = (RemoteAddeTableModel)remoteTable.getModel();
354            int first = Integer.MAX_VALUE;
355            int last = Integer.MIN_VALUE;
356            for (RemoteAddeEntry entry : removable) {
357                int index = tableModel.getRowForEntry(entry);
358                if (index >= 0) {
359                    if (index < first) {
360                        first = index;
361                    }
362                    if (index > last) {
363                        last = index;
364                    }
365                }
366            }
367            tableModel.fireTableDataChanged();
368            refreshDisplay();
369            remoteTable.revalidate();
370            if (first < remoteTable.getRowCount()) {
371                remoteTable.setRowSelectionInterval(first, first);
372            }
373        } else {
374            logger.debug("could not remove entries={}", removable);
375        }
376    }
377
378    /**
379     * Shows a local ADDE entry editor <b>without</b> anything pre-populated 
380     * (creating a new local ADDE dataset).
381     */
382    public void showLocalEditor() {
383        // TODO(jon): differentiate between showLocalEditor() and showLocalEditor(entry)
384        if (tabbedPane.getSelectedIndex() != 1) {
385            tabbedPane.setSelectedIndex(1);
386        }
387        LocalEntryEditor editor = new LocalEntryEditor(this, true, this, serverManager);
388        editor.setVisible(true);
389    }
390
391    /**
392     * Shows a local ADDE entry editor <b>with</b> the appropriate fields
393     * pre-populated, using the values from {@code entry}. This is intended to 
394     * handle {@literal "editing"} a local ADDE dataset.
395     * 
396     * @param entry Entry to edit; should not be {@code null}.
397     */
398    public void showLocalEditor(final LocalAddeEntry entry) {
399        // TODO(jon): differentiate between showLocalEditor() and showLocalEditor(entry)
400        if (tabbedPane.getSelectedIndex() != 1) {
401            tabbedPane.setSelectedIndex(1);
402        }
403        LocalEntryEditor editor = new LocalEntryEditor(this, true, this, serverManager, entry);
404        editor.setVisible(true);
405    }
406
407    /**
408     * Removes the given local ADDE entries from the server manager GUI.
409     * 
410     * @param entries Entries to remove. {@code null} is permissible, but is a {@literal "no-op"}.
411     */
412    public void removeLocalEntries(final Collection<LocalAddeEntry> entries) {
413        if (entries == null) {
414            return;
415        }
416//        if (entrySet.removeAll(entries)) {
417        if (serverManager.removeEntries(entries)) {
418            logger.trace("successful removal of entries={}",entries);
419            LocalAddeTableModel tableModel = (LocalAddeTableModel)localTable.getModel();
420            int first = Integer.MAX_VALUE;
421            int last = Integer.MIN_VALUE;
422            for (LocalAddeEntry entry : entries) {
423                int index = tableModel.getRowForEntry(entry);
424                if (index >= 0) {
425                    if (index < first) {
426                        first = index;
427                    }
428                    if (index > last) {
429                        last = index;
430                    }
431                }
432            }
433            tableModel.fireTableDataChanged();
434            refreshDisplay();
435            localTable.revalidate();
436            if (first < localTable.getRowCount()) {
437                localTable.setRowSelectionInterval(first, first);
438            }
439        } else {
440            logger.debug("could not remove entries={}", entries);
441        }
442    }
443
444    /**
445     * Extracts datasets from a given MCTABLE.TXT and adds them to the server
446     * manager.
447     * 
448     * @param path Path to the MCTABLE.TXT. Cannot be {@code null}.
449     * @param username ADDE username to use for verifying extracted datasets. Cannot be {@code null}.
450     * @param project ADDE project number to use for verifying extracted datasets. Cannot be {@code null}.
451     */
452    public void importMctable(final String path, final String username, final String project) {
453        logger.trace("extracting path={} username={}, project={}", new Object[] { path, username, project });
454        final Set<RemoteAddeEntry> imported = EntryTransforms.extractMctableEntries(path, username, project);
455        logger.trace("extracted entries={}", imported);
456        if (imported.equals(Collections.emptySet())) {
457            LogUtil.userErrorMessage("Selection does not appear to a valid MCTABLE.TXT file:\n"+path);
458        } else {
459            logger.trace("adding extracted entries...");
460            // verify entries first!
461            serverManager.addEntries(imported);
462            refreshDisplay();
463            repaint();
464            Runnable r = new Runnable() {
465                public void run() {
466                    checkDatasets(imported);
467                }
468            };
469            Thread t = new Thread(r);
470            t.start();
471        }
472    }
473
474    /**
475     * Attempts to start the local servers. 
476     * 
477     * @see EntryStore#startLocalServer()
478     */
479    public void startLocalServers() {
480        logger.trace("starting local servers...?");
481        serverManager.startLocalServer();
482    }
483
484    /**
485     * Attempts to stop the local servers.
486     * 
487     * @see EntryStore#stopLocalServer()
488     */
489    public void stopLocalServers() {
490        logger.trace("stopping local servers...?");
491        serverManager.stopLocalServer();
492    }
493
494    /**
495     * Responds to local server events and attempts to update the GUI status
496     * message.
497     * 
498     * @param event Local server event. Should not be {@code null}.
499     */
500    @EventSubscriber(eventClass=AddeThread.McservEvent.class)
501    public void mcservUpdated(final AddeThread.McservEvent event) {
502        logger.trace("eventbus evt={}", event.toString());
503        final String msg;
504        switch (event) {
505            case ACTIVE: case DIED: case STOPPED:
506                msg = event.getMessage();
507                break;
508            case STARTED:
509//                msg = "Local servers are listening on port "+EntryStore.getLocalPort();
510                msg = String.format(event.getMessage(),EntryStore.getLocalPort());
511                break;
512            default:
513                msg = "Unknown local servers status: "+event.toString();
514                break;
515        }
516        SwingUtilities.invokeLater(new Runnable() {
517            public void run() {
518                if (statusLabel != null) {
519                    statusLabel.setText(msg);
520                }
521            }
522        });
523    }
524
525    /**
526     * Builds the server manager GUI.
527     */
528    @SuppressWarnings({"unchecked", "FeatureEnvy", "MagicNumber"})
529    public void initComponents() {
530        Dimension frameSize = new Dimension(730, 460);
531        ucar.unidata.ui.Help.setTopDir(HELP_TOP_DIR);
532        system = icon("/edu/wisc/ssec/mcidasv/resources/icons/servermanager/padlock_closed.png");
533        mctable = icon("/edu/wisc/ssec/mcidasv/resources/icons/servermanager/bug.png");
534        user = icon("/edu/wisc/ssec/mcidasv/resources/icons/servermanager/hand_pro.png");
535        invalid = icon("/edu/wisc/ssec/mcidasv/resources/icons/servermanager/emotion_sad.png");
536        unverified = icon("/edu/wisc/ssec/mcidasv/resources/icons/servermanager/eye_inv.png");
537        setTitle("ADDE Data Manager");
538        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
539        setSize(frameSize);
540        setMinimumSize(frameSize);
541        addWindowListener(new WindowAdapter() {
542            public void windowClosed(WindowEvent evt) {
543                formWindowClosed(evt);
544            }
545        });
546
547        JMenuBar menuBar = new JMenuBar();
548        setJMenuBar(menuBar);
549
550        JMenu fileMenu = new JMenu("File");
551        menuBar.add(fileMenu);
552
553        JMenuItem remoteNewMenuItem = new JMenuItem("New Remote Dataset");
554        remoteNewMenuItem.addActionListener(new java.awt.event.ActionListener() {
555            public void actionPerformed(ActionEvent evt) {
556                showRemoteEditor();
557            }
558        });
559        fileMenu.add(remoteNewMenuItem);
560
561        JMenuItem localNewMenuItem = new JMenuItem("New Local Dataset");
562        localNewMenuItem.addActionListener(new java.awt.event.ActionListener() {
563            public void actionPerformed(ActionEvent evt) {
564                showLocalEditor();
565            }
566        });
567        fileMenu.add(localNewMenuItem);
568
569        fileMenu.add(new JSeparator());
570
571        JMenuItem importMctableMenuItem = new JMenuItem("Import MCTABLE...");
572        importMctableMenuItem.addActionListener(new ActionListener() {
573            public void actionPerformed(ActionEvent e) {
574                importButtonActionPerformed(e);
575            }
576        });
577        fileMenu.add(importMctableMenuItem);
578
579        JMenuItem importUrlMenuItem = new JMenuItem("Import from URL...");
580        final TabbedAddeManager myRef = this;
581        importUrlMenuItem.addActionListener(new ActionListener() {
582            public void actionPerformed(ActionEvent e) {
583                SwingUtilities.invokeLater(new Runnable() {
584                   public void run() { 
585                       try {
586                           ImportUrl dialog = new ImportUrl(serverManager, myRef);
587                           dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
588                           dialog.setVisible(true);
589                       } catch (Exception e) {
590                           e.printStackTrace();
591                       }
592                   }
593                });
594            }
595        });
596        fileMenu.add(importUrlMenuItem);
597
598//        JMenuItem exportMenuItem = new JMenuItem("Export...");
599//        exportMenuItem.addActionListener(new ActionListener() {
600//            public void actionPerformed(ActionEvent e) {
601//                logger.trace("exporting datasets...");
602//            }
603//        });
604//        fileMenu.add(exportMenuItem);
605//
606        fileMenu.add(new JSeparator());
607
608        JMenuItem closeMenuItem = new JMenuItem("Close");
609        closeMenuItem.addActionListener(new java.awt.event.ActionListener() {
610            public void actionPerformed(ActionEvent evt) {
611                logger.debug("evt={}", evt.toString());
612                closeManager();
613            }
614        });
615        fileMenu.add(closeMenuItem);
616
617        JMenu editMenu = new JMenu("Edit");
618        menuBar.add(editMenu);
619
620        editMenuItem = new JMenuItem("Edit Entry...");
621        editMenuItem.setEnabled(false);
622        editMenuItem.addActionListener(new java.awt.event.ActionListener() {
623            public void actionPerformed(ActionEvent evt) {
624                if (tabbedPane.getSelectedIndex() == 0) {
625                    showRemoteEditor(getSelectedRemoteEntries());
626                } else {
627                    showLocalEditor(getSingleLocalSelection());
628                }
629            }
630        });
631        editMenu.add(editMenuItem);
632
633        removeMenuItem = new JMenuItem("Remove Selection");
634        removeMenuItem.setEnabled(false);
635        removeMenuItem.addActionListener(new java.awt.event.ActionListener() {
636            public void actionPerformed(ActionEvent evt) {
637                if (tabbedPane.getSelectedIndex() == 0) {
638                    removeRemoteEntries(getSelectedRemoteEntries());
639                } else {
640                    removeLocalEntries(getSelectedLocalEntries());
641                }
642            }
643        });
644        editMenu.add(removeMenuItem);
645
646        JMenu localServersMenu = new JMenu("Local Servers");
647        menuBar.add(localServersMenu);
648
649        JMenuItem startLocalMenuItem = new JMenuItem("Start Local Servers");
650        startLocalMenuItem.addActionListener(new ActionListener() {
651            public void actionPerformed(ActionEvent e) {
652                startLocalServers();
653            }
654        });
655        localServersMenu.add(startLocalMenuItem);
656
657        JMenuItem stopLocalMenuItem = new JMenuItem("Stop Local Servers");
658        stopLocalMenuItem.addActionListener(new ActionListener() {
659            public void actionPerformed(ActionEvent e) {
660                stopLocalServers();
661            }
662        });
663        localServersMenu.add(stopLocalMenuItem);
664
665        JMenu helpMenu = new JMenu("Help");
666        menuBar.add(helpMenu);
667
668        JMenuItem remoteHelpMenuItem = new JMenuItem("Show Remote Data Help");
669        remoteHelpMenuItem.addActionListener(new java.awt.event.ActionListener() {
670            public void actionPerformed(ActionEvent evt) {
671                ucar.unidata.ui.Help.getDefaultHelp().gotoTarget(REMOTE_HELP_TARGET);
672            }
673        });
674        helpMenu.add(remoteHelpMenuItem);
675
676        JMenuItem localHelpMenuItem = new JMenuItem("Show Local Data Help");
677        localHelpMenuItem.addActionListener(new java.awt.event.ActionListener() {
678            public void actionPerformed(ActionEvent evt) {
679                ucar.unidata.ui.Help.getDefaultHelp().gotoTarget(LOCAL_HELP_TARGET);
680            }
681        });
682        helpMenu.add(localHelpMenuItem);
683
684        contentPane = new JPanel();
685        contentPane.setBorder(null);
686        setContentPane(contentPane);
687        contentPane.setLayout(new MigLayout("", "[grow]", "[grow][grow][grow]"));
688
689        tabbedPane = new JTabbedPane(JTabbedPane.TOP);
690        tabbedPane.addChangeListener(new ChangeListener() {
691            public void stateChanged(ChangeEvent event) {
692                handleTabStateChanged(event);
693            }
694        });
695        contentPane.add(tabbedPane, "cell 0 0 1 3,grow");
696
697        JPanel remoteTab = new JPanel();
698        remoteTab.setBorder(new EmptyBorder(0, 4, 4, 4));
699        tabbedPane.addTab("Remote Data", null, remoteTab, null);
700        remoteTab.setLayout(new BoxLayout(remoteTab, BoxLayout.Y_AXIS));
701
702        remoteTable = new BetterJTable();
703        JScrollPane remoteScroller = BetterJTable.createStripedJScrollPane(remoteTable);
704
705        remoteTable.setModel(new RemoteAddeTableModel(serverManager));
706        remoteTable.setColumnSelectionAllowed(false);
707        remoteTable.setRowSelectionAllowed(true);
708        remoteTable.getTableHeader().setReorderingAllowed(false);
709        remoteTable.setFont(UIManager.getFont("Table.font").deriveFont(11.0f));
710        remoteTable.getColumnModel().getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
711        remoteTable.setDefaultRenderer(String.class, new TextRenderer());
712        remoteTable.getColumnModel().getColumn(0).setPreferredWidth(10);
713        remoteTable.getColumnModel().getColumn(1).setPreferredWidth(10);
714        remoteTable.getColumnModel().getColumn(3).setPreferredWidth(50);
715        remoteTable.getColumnModel().getColumn(4).setPreferredWidth(50);
716        remoteTable.getColumnModel().getColumn(0).setCellRenderer(new EntryValidityRenderer());
717        remoteTable.getColumnModel().getColumn(1).setCellRenderer(new EntrySourceRenderer());
718        remoteTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
719            public void valueChanged(final ListSelectionEvent e) {
720                remoteSelectionModelChanged(e);
721            }
722        });
723        remoteTable.addMouseListener(new java.awt.event.MouseAdapter() {
724            public void mouseClicked(final java.awt.event.MouseEvent e) {
725                if ((e.getClickCount() == 2) && (hasSingleRemoteSelection())) {
726                    showRemoteEditor(getSelectedRemoteEntries());
727                }
728            }
729        });
730        remoteScroller.setViewportView(remoteTable);
731        remoteTab.add(remoteScroller);
732
733        JPanel remoteActionPanel = new JPanel();
734        remoteTab.add(remoteActionPanel);
735        remoteActionPanel.setLayout(new BoxLayout(remoteActionPanel, BoxLayout.X_AXIS));
736
737        newRemoteButton = new JButton("Add New Dataset");
738        newRemoteButton.addActionListener(new ActionListener() {
739            public void actionPerformed(ActionEvent e) {
740                logger.trace("new remote dataset");
741                showRemoteEditor();
742            }
743        });
744        newRemoteButton.setToolTipText("Create a new remote ADDE dataset.");
745        remoteActionPanel.add(newRemoteButton);
746
747        editRemoteButton = new JButton("Edit Dataset");
748        editRemoteButton.addActionListener(new ActionListener() {
749            public void actionPerformed(ActionEvent e) {
750                logger.trace("edit remote dataset");
751                showRemoteEditor(getSelectedRemoteEntries());
752            }
753        });
754        editRemoteButton.setToolTipText("Edit an existing remote ADDE dataset.");
755        remoteActionPanel.add(editRemoteButton);
756
757        removeRemoteButton = new JButton("Remove Selection");
758        removeRemoteButton.addActionListener(new ActionListener() {
759            public void actionPerformed(ActionEvent e) {
760                logger.trace("remove remote dataset");
761                removeRemoteEntries(getSelectedRemoteEntries());
762            }
763        });
764        removeRemoteButton.setToolTipText("Remove the selected remote ADDE datasets.");
765        remoteActionPanel.add(removeRemoteButton);
766
767        importRemoteButton = new JButton("Import MCTABLE...");
768        importRemoteButton.addActionListener(new ActionListener() {
769            public void actionPerformed(ActionEvent e) {
770                logger.trace("import from mctable...");
771                importButtonActionPerformed(e);
772            }
773        });
774        remoteActionPanel.add(importRemoteButton);
775
776        JPanel localTab = new JPanel();
777        localTab.setBorder(new EmptyBorder(0, 4, 4, 4));
778        tabbedPane.addTab("Local Data", null, localTab, null);
779        localTab.setLayout(new BoxLayout(localTab, BoxLayout.Y_AXIS));
780
781        localTable = new BetterJTable();
782        JScrollPane localScroller = BetterJTable.createStripedJScrollPane(localTable);
783        localTable.setModel(new LocalAddeTableModel(serverManager));
784        localTable.setColumnSelectionAllowed(false);
785        localTable.setRowSelectionAllowed(true);
786        localTable.getTableHeader().setReorderingAllowed(false);
787        localTable.setFont(UIManager.getFont("Table.font").deriveFont(11.0f));
788        localTable.setDefaultRenderer(String.class, new TextRenderer());
789        localTable.getColumnModel().getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
790        localTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
791            public void valueChanged(final ListSelectionEvent e) {
792                localSelectionModelChanged(e);
793            }
794        });
795        localTable.addMouseListener(new java.awt.event.MouseAdapter() {
796            public void mouseClicked(final java.awt.event.MouseEvent e) {
797                if ((e.getClickCount() == 2) && (hasSingleLocalSelection())) {
798                    showLocalEditor(getSingleLocalSelection());
799                }
800            }
801        });
802        localScroller.setViewportView(localTable);
803        localTab.add(localScroller);
804
805        JPanel localActionPanel = new JPanel();
806        localTab.add(localActionPanel);
807        localActionPanel.setLayout(new BoxLayout(localActionPanel, BoxLayout.X_AXIS));
808
809        newLocalButton = new JButton("Add New Dataset");
810        newLocalButton.addActionListener(new ActionListener() {
811            public void actionPerformed(ActionEvent e) {
812                logger.trace("new local dataset");
813                showLocalEditor();
814            }
815        });
816        newLocalButton.setToolTipText("Create a new local ADDE dataset.");
817        localActionPanel.add(newLocalButton);
818
819        editLocalButton = new JButton("Edit Dataset");
820        editLocalButton.setEnabled(false);
821        editLocalButton.addActionListener(new ActionListener() {
822            public void actionPerformed(ActionEvent e) {
823                logger.trace("edit local dataset");
824                showLocalEditor(getSingleLocalSelection());
825            }
826        });
827        editLocalButton.setToolTipText("Edit an existing local ADDE dataset.");
828        localActionPanel.add(editLocalButton);
829
830        removeLocalButton = new JButton("Remove Selection");
831        removeLocalButton.setEnabled(false);
832        removeLocalButton.addActionListener(new ActionListener() {
833            public void actionPerformed(ActionEvent e) {
834                logger.trace("remove local dataset");
835                removeLocalEntries(getSelectedLocalEntries());
836            }
837        });
838        removeLocalButton.setToolTipText("Remove the selected local ADDE datasets.");
839        localActionPanel.add(removeLocalButton);
840
841        JPanel statusPanel = new JPanel();
842        statusPanel.setBorder(new EmptyBorder(0, 6, 0, 6));
843        contentPane.add(statusPanel, "cell 0 3,grow");
844        statusPanel.setLayout(new BorderLayout(0, 0));
845
846        Box statusMessageBox = Box.createHorizontalBox();
847        statusPanel.add(statusMessageBox, BorderLayout.WEST);
848
849        String statusMessage = McservEvent.STOPPED.getMessage();
850        if (serverManager.checkLocalServer()) {
851            statusMessage = McservEvent.ACTIVE.getMessage();
852        }
853        statusLabel = new JLabel(statusMessage);
854        statusMessageBox.add(statusLabel);
855        statusLabel.setEnabled(false);
856
857        Box frameControlBox = Box.createHorizontalBox();
858        statusPanel.add(frameControlBox, BorderLayout.EAST);
859
860//        cancelButton = new JButton("Cancel");
861//        cancelButton.addActionListener(new ActionListener() {
862//            public void actionPerformed(ActionEvent e) {
863//                handleCancellingChanges();
864//            }
865//        });
866//        frameControlBox.add(cancelButton);
867
868//        applyButton = new JButton("Apply");
869//        applyButton.addActionListener(new ActionListener() {
870//            public void actionPerformed(ActionEvent e) {
871//                logger.trace("apply");
872//                handleSavingChanges();
873//            }
874//        });
875//        frameControlBox.add(applyButton);
876
877        okButton = new JButton("Ok");
878        okButton.addActionListener(new ActionListener() {
879            public void actionPerformed(ActionEvent e) {
880//                handleSavingChanges();
881                closeManager();
882            }
883        });
884        frameControlBox.add(okButton);
885        tabbedPane.setSelectedIndex(getLastTab());
886        guiInitialized = true;
887    }
888
889//    /**
890//     * Determines whether or not the user has changed anything (where {@literal "changed"} means added, modified, or removed entries).
891//     * 
892//     * @return {@code true} if the user has changed any entries; {@code false} otherwise.
893//     */
894//    public boolean hasUserChanges() {
895//        return !entrySet.equals(serverManager.getEntrySet());
896//    }
897
898//    /**
899//     * Respond to the user clicking the {@literal "cancel"} button.
900//     */
901//    public void handleCancellingChanges() {
902//        logger.trace("cancel changes. anything to do={}", hasUserChanges());
903//        closeManager();
904//    }
905//
906//    /**
907//     * Respond to the user clicking the {@literal "save changes"} button.
908//     */
909//    public void handleSavingChanges() {
910//        boolean userChanges = hasUserChanges();
911//        logger.trace("save changes. anything to do={}", userChanges);
912//        if (userChanges) {
913//            serverManager.addEntries(entrySet);
914//        }
915//    }
916
917    /**
918     * Respond to changes in {@link #tabbedPane}; primarily switching tabs.
919     * 
920     * @param event Event being handled. Ignored for now.
921     */
922    private void handleTabStateChanged(final ChangeEvent event) {
923        assert SwingUtilities.isEventDispatchThread();
924        boolean hasSelection = false;
925        int index = 0;
926        if (guiInitialized) {
927            index = tabbedPane.getSelectedIndex();
928            if (index == 0) {
929                hasSelection = hasRemoteSelection();
930                editRemoteButton.setEnabled(hasSelection);
931                removeRemoteButton.setEnabled(hasSelection);
932            } else {
933                hasSelection = hasLocalSelection();
934                editLocalButton.setEnabled(hasSelection);
935                removeLocalButton.setEnabled(hasSelection);
936            }
937            editMenuItem.setEnabled(hasSelection);
938            removeMenuItem.setEnabled(hasSelection);
939            setLastTab(index);
940        }
941        logger.trace("index={} hasRemote={} hasLocal={} guiInit={}", new Object[] {index, hasRemoteSelection(), hasLocalSelection(), guiInitialized});
942    }
943
944    /**
945     * Respond to events.
946     * 
947     * @param e {@link ListSelectionEvent} that necessitated this call.
948     */
949    private void remoteSelectionModelChanged(final ListSelectionEvent e) {
950        if (e.getValueIsAdjusting()) {
951            return;
952        }
953
954        int selectedRowCount = 0;
955        ListSelectionModel selModel = (ListSelectionModel)e.getSource();
956        Set<RemoteAddeEntry> selectedEntries;
957        if (selModel.isSelectionEmpty()) {
958            selectedEntries = Collections.emptySet();
959        } else {
960            int min = selModel.getMinSelectionIndex();
961            int max = selModel.getMaxSelectionIndex();
962            RemoteAddeTableModel tableModel = (RemoteAddeTableModel)remoteTable.getModel();
963            selectedEntries = newLinkedHashSet((max - min) * AddeEntry.EntryType.values().length);
964            for (int i = min; i <= max; i++) {
965                if (selModel.isSelectedIndex(i)) {
966                    List<RemoteAddeEntry> entries = tableModel.getEntriesAtRow(i);
967                    selectedEntries.addAll(entries);
968                    selectedRowCount++;
969                }
970            }
971        }
972
973        boolean onlyDefaultEntries = true;
974        for (RemoteAddeEntry entry : selectedEntries) {
975            if (entry.getEntrySource() != EntrySource.SYSTEM) {
976                onlyDefaultEntries = false;
977                break;
978            }
979        }
980        setSelectedRemoteEntries(selectedEntries);
981
982        // the current "edit" dialog doesn't work so well with multiple 
983        // servers/datasets, so only allow the user to edit entries one at a time.
984        boolean singleSelection = selectedRowCount == 1;
985        editRemoteButton.setEnabled(singleSelection);
986        editMenuItem.setEnabled(singleSelection);
987
988        boolean hasSelection = (selectedRowCount >= 1) && !onlyDefaultEntries;
989        removeRemoteButton.setEnabled(hasSelection);
990        removeMenuItem.setEnabled(hasSelection);
991    }
992
993    /**
994     * Respond to events from the local dataset table.
995     * 
996     * @param e {@link ListSelectionEvent} that necessitated this call.
997     */
998    private void localSelectionModelChanged(final ListSelectionEvent e) {
999        if (e.getValueIsAdjusting()) {
1000            return;
1001        }
1002        ListSelectionModel selModel = (ListSelectionModel)e.getSource();
1003        Set<LocalAddeEntry> selectedEntries;
1004        if (selModel.isSelectionEmpty()) {
1005            selectedEntries = Collections.emptySet();
1006        } else {
1007            int min = selModel.getMinSelectionIndex();
1008            int max = selModel.getMaxSelectionIndex();
1009            LocalAddeTableModel tableModel = (LocalAddeTableModel)localTable.getModel();
1010            selectedEntries = newLinkedHashSet(max - min);
1011            for (int i = min; i <= max; i++) {
1012                if (selModel.isSelectedIndex(i)) {
1013                    selectedEntries.add(tableModel.getEntryAtRow(i));
1014                }
1015            }
1016        }
1017
1018        setSelectedLocalEntries(selectedEntries);
1019
1020        // the current "edit" dialog doesn't work so well with multiple 
1021        // servers/datasets, so only allow the user to edit entries one at a time.
1022        boolean singleSelection = selectedEntries.size() == 1;
1023        this.editRemoteButton.setEnabled(singleSelection);
1024        this.editMenuItem.setEnabled(singleSelection);
1025
1026        boolean hasSelection = !selectedEntries.isEmpty();
1027        removeRemoteButton.setEnabled(hasSelection);
1028        removeMenuItem.setEnabled(hasSelection);
1029    }
1030
1031    /**
1032     * Checks to see if {@link #selectedRemoteEntries} contains any 
1033     * {@link RemoteAddeEntry}s.
1034     *
1035     * @return Whether or not any {@code RemoteAddeEntry} values are selected.
1036     */
1037    private boolean hasRemoteSelection() {
1038        return !selectedRemoteEntries.isEmpty();
1039    }
1040
1041    /**
1042     * Checks to see if {@link #selectedLocalEntries} contains any
1043     * {@link LocalAddeEntry}s.
1044     *
1045     * @return Whether or not any {@code LocalAddeEntry} values are selected.
1046     */
1047    private boolean hasLocalSelection() {
1048        return !selectedLocalEntries.isEmpty();
1049    }
1050
1051    /**
1052     * Checks to see if the user has select a <b>single</b> remote dataset.
1053     * 
1054     * @return {@code true} if there is a single remote dataset selected. {@code false} otherwise.
1055     */
1056    private boolean hasSingleRemoteSelection() {
1057        String entryText = null;
1058        for (RemoteAddeEntry entry : selectedRemoteEntries) {
1059            if (entryText == null) {
1060                entryText = entry.getEntryText();
1061            }
1062            if (!entry.getEntryText().equals(entryText)) {
1063                return false;
1064            }
1065        }
1066        return true;
1067    }
1068
1069    /**
1070     * Checks to see if the user has select a <b>single</b> local dataset.
1071     * 
1072     * @return {@code true} if there is a single local dataset selected. {@code false} otherwise.
1073     */
1074    private boolean hasSingleLocalSelection() {
1075        return selectedLocalEntries.size() == 1;
1076    }
1077
1078    /**
1079     * If there is a single local dataset selected, this method will return that
1080     * dataset.
1081     * 
1082     * @return Either the single selected local dataset, or {@link LocalAddeEntry#INVALID_ENTRY}.
1083     */
1084    private LocalAddeEntry getSingleLocalSelection() {
1085        LocalAddeEntry entry = LocalAddeEntry.INVALID_ENTRY;
1086        if (selectedLocalEntries.size() == 1) {
1087            entry = selectedLocalEntries.get(0);
1088        }
1089        return entry;
1090    }
1091
1092    /**
1093     * Corresponds to the selected remote ADDE entries in the GUI.
1094     * 
1095     * @param entries Should not be {@code null}.
1096     */
1097    private void setSelectedRemoteEntries(final Collection<RemoteAddeEntry> entries) {
1098        selectedRemoteEntries.clear();
1099        selectedRemoteEntries.addAll(entries);
1100        this.editRemoteButton.setEnabled(entries.size() == 1);
1101        this.removeRemoteButton.setEnabled(!entries.isEmpty());
1102        logger.trace("remote entries={}", entries);
1103    }
1104
1105    /**
1106     * Gets the selected remote ADDE entries.
1107     * 
1108     * @return Either an empty list or the remote entries selected in the GUI.
1109     */
1110    private List<RemoteAddeEntry> getSelectedRemoteEntries() {
1111        if (selectedRemoteEntries.isEmpty()) {
1112            return Collections.emptyList();
1113        } else {
1114            return arrList(selectedRemoteEntries);
1115        }
1116    }
1117
1118    /**
1119     * Corresponds to the selected local ADDE entries in the GUI.
1120     * 
1121     * @param entries Should not be {@code null}.
1122     */
1123    private void setSelectedLocalEntries(final Collection<LocalAddeEntry> entries) {
1124        selectedLocalEntries.clear();
1125        selectedLocalEntries.addAll(entries);
1126        this.editLocalButton.setEnabled(entries.size() == 1);
1127        this.removeLocalButton.setEnabled(!entries.isEmpty());
1128        logger.trace("local entries={}", entries);
1129    }
1130
1131    /**
1132     * Gets the selected local ADDE entries.
1133     * 
1134     * @return Either an empty list or the local entries selected in the GUI.
1135     */
1136    private List<LocalAddeEntry> getSelectedLocalEntries() {
1137        if (selectedLocalEntries.isEmpty()) {
1138            return Collections.emptyList();
1139        } else {
1140            return arrList(selectedLocalEntries);
1141        }
1142    }
1143
1144    /**
1145     * Handles the user closing the server manager GUI.
1146     * 
1147     * @param evt Event that triggered this method call.
1148     * 
1149     * @see #closeManager()
1150     */
1151    private void formWindowClosed(WindowEvent evt) {
1152        logger.debug("evt={}", evt.toString());
1153        closeManager();
1154    }
1155
1156    @SuppressWarnings({"MagicNumber"})
1157    private JPanel makeFileChooserAccessory() {
1158        assert SwingUtilities.isEventDispatchThread();
1159        JPanel accessory = new JPanel();
1160        accessory.setLayout(new BoxLayout(accessory, BoxLayout.PAGE_AXIS));
1161        importAccountBox = new JCheckBox("Use ADDE Accounting?");
1162        importAccountBox.setSelected(false);
1163        importAccountBox.addActionListener(new java.awt.event.ActionListener() {
1164            public void actionPerformed(ActionEvent evt) {
1165                boolean selected = importAccountBox.isSelected();
1166                importUser.setEnabled(selected);
1167                importProject.setEnabled(selected);
1168            }
1169        });
1170        String clientProp = "JComponent.sizeVariant";
1171        String propVal = "mini";
1172
1173        importUser = new JTextField();
1174        importUser.putClientProperty(clientProp, propVal);
1175        Prompt userPrompt = new Prompt(importUser, "Username");
1176        userPrompt.putClientProperty(clientProp, propVal);
1177        importUser.setEnabled(importAccountBox.isSelected());
1178
1179        importProject = new JTextField();
1180        Prompt projPrompt = new Prompt(importProject, "Project Number");
1181        projPrompt.putClientProperty(clientProp, propVal);
1182        importProject.putClientProperty(clientProp, propVal);
1183        importProject.setEnabled(importAccountBox.isSelected());
1184
1185        GroupLayout layout = new GroupLayout(accessory);
1186        accessory.setLayout(layout);
1187        layout.setHorizontalGroup(
1188            layout.createParallelGroup(GroupLayout.Alignment.LEADING)
1189            .addComponent(importAccountBox)
1190            .addGroup(layout.createSequentialGroup()
1191                .addContainerGap()
1192                .addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING, false)
1193                    .addComponent(importProject, GroupLayout.Alignment.LEADING)
1194                    .addComponent(importUser, GroupLayout.Alignment.LEADING, GroupLayout.DEFAULT_SIZE, 131, Short.MAX_VALUE)))
1195        );
1196        layout.setVerticalGroup(
1197            layout.createParallelGroup(GroupLayout.Alignment.LEADING)
1198            .addGroup(layout.createSequentialGroup()
1199                .addComponent(importAccountBox)
1200                .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
1201                .addComponent(importUser, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
1202                .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
1203                .addComponent(importProject, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
1204                .addContainerGap(55, (int)Short.MAX_VALUE))
1205        );
1206        return accessory;
1207    }
1208
1209    private void importButtonActionPerformed(ActionEvent evt) {
1210        assert SwingUtilities.isEventDispatchThread();
1211        JFileChooser fc = new JFileChooser(getLastImportPath());
1212        fc.setAccessory(makeFileChooserAccessory());
1213        fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
1214        int ret = fc.showOpenDialog(TabbedAddeManager.this);
1215        if (ret == JFileChooser.APPROVE_OPTION) {
1216            File f = fc.getSelectedFile();
1217            String path = f.getPath();
1218
1219            boolean defaultUser = false;
1220            String forceUser = safeGetText(importUser);
1221            if (forceUser.isEmpty()) {
1222                forceUser = AddeEntry.DEFAULT_ACCOUNT.getUsername();
1223                defaultUser = true;
1224            }
1225
1226            boolean defaultProj = false;
1227            String forceProj = safeGetText(importProject);
1228            if (forceProj.isEmpty()) {
1229                forceProj = AddeEntry.DEFAULT_ACCOUNT.getProject();
1230                defaultProj = true;
1231            }
1232
1233            
1234            if (importAccountBox.isSelected() && (defaultUser || defaultProj)) {
1235                logger.warn("bad acct dialog: forceUser={} forceProj={}", forceUser, forceProj);
1236            } else {
1237                logger.warn("acct appears valid: forceUser={} forceProj={}", forceUser, forceProj);
1238                importMctable(path, forceUser, forceProj);
1239                // don't worry about file validity; i'll just assume the user clicked
1240                // on the wrong entry by accident.
1241                setLastImportPath(f.getParent());
1242            }
1243        }
1244    }
1245
1246    /**
1247     * Returns the directory that contained the most recently imported MCTABLE.TXT.
1248     *
1249     * @return Either the path to the most recently imported MCTABLE.TXT file,
1250     * or an empty {@code String}.
1251     */
1252    private String getLastImportPath() {
1253        String lastPath = serverManager.getIdvStore().get(LAST_IMPORTED, "");
1254        logger.trace("last path='{}'", lastPath);
1255        return lastPath;
1256//        return serverManager.getIdvStore().get(LAST_IMPORTED, "");
1257    }
1258
1259    /**
1260     * Saves the directory that contained the most recently imported MCTABLE.TXT.
1261     *
1262     * @param path Path to the most recently imported MCTABLE.TXT file.
1263     * {@code null} values are replaced with an empty {@code String}.
1264     */
1265    private void setLastImportPath(final String path) {
1266        String okayPath = (path == null) ? "" : path;
1267        logger.trace("saving path='{}'", okayPath);
1268        serverManager.getIdvStore().put(LAST_IMPORTED, okayPath);
1269    }
1270
1271    /**
1272     * Returns the index of the user's last server manager tab.
1273     *
1274     * @return Index of the user's most recently viewed server manager tab, or {@code 0}.
1275     */
1276    private int getLastTab() {
1277        int index = serverManager.getIdvStore().get(LAST_TAB, 0);
1278        logger.trace("last tab={}", index);
1279        return index;
1280//        return serverManager.getIdvStore().get(LAST_TAB, 0);
1281    }
1282
1283    /**
1284     * Saves the index of the last server manager tab the user was looking at.
1285     * 
1286     * @param index Index of the user's most recently viewed server manager tab.
1287     */
1288    private void setLastTab(final int index) {
1289        int okayIndex = ((index >= 0) && (index < 2)) ? index : 0;
1290        IdvObjectStore store = serverManager.getIdvStore();
1291        logger.trace("storing tab={}", okayIndex);
1292        store.put(LAST_TAB, okayIndex);
1293    }
1294
1295    // stupid adde.ucar.edu entries never seem to time out! great! making the gui hang is just so awesome!
1296    @SuppressWarnings({"ObjectAllocationInLoop"})
1297    public Set<RemoteAddeEntry> checkDatasets(final Collection<RemoteAddeEntry> entries) {
1298        requireNonNull(entries, "can't check a null collection of entries");
1299        if (entries.isEmpty()) {
1300            return Collections.emptySet();
1301        }
1302
1303        Set<RemoteAddeEntry> valid = newLinkedHashSet();
1304        ExecutorService exec = Executors.newFixedThreadPool(POOL);
1305        CompletionService<List<RemoteAddeEntry>> ecs = new ExecutorCompletionService<List<RemoteAddeEntry>>(exec);
1306        final RemoteAddeTableModel tableModel = (RemoteAddeTableModel)remoteTable.getModel();
1307
1308        // place entries
1309        for (RemoteAddeEntry entry : entries) {
1310            ecs.submit(new BetterCheckTask(entry));
1311            logger.trace("submitting entry={}", entry);
1312            final int row = tableModel.getRowForEntry(entry);
1313            runOnEDT(new Runnable() {
1314                public void run() {
1315                    tableModel.fireTableRowsUpdated(row, row);
1316                }
1317            });
1318        }
1319
1320        // work through the entries
1321        try {
1322            for (int i = 0; i < entries.size(); i++) {
1323                final List<RemoteAddeEntry> checkedEntries = ecs.take().get();
1324                if (!checkedEntries.isEmpty()) {
1325                    final int row = tableModel.getRowForEntry(checkedEntries.get(0));
1326                    runOnEDT(new Runnable() {
1327                        public void run() {
1328                            List<RemoteAddeEntry> oldEntries = tableModel.getEntriesAtRow(row);
1329                            serverManager.replaceEntries(oldEntries, checkedEntries);
1330//                            entrySet.removeAll(oldEntries);
1331//                            entrySet.addAll(checkedEntries);
1332                            tableModel.fireTableRowsUpdated(row, row);
1333                        }
1334                    });
1335                }
1336                valid.addAll(checkedEntries);
1337            }
1338        } catch (InterruptedException e) {
1339            LogUtil.logException("Interrupted while validating entries", e);
1340        } catch (ExecutionException e) {
1341            LogUtil.logException("ADDE validation execution error", e);
1342        } finally {
1343            exec.shutdown();
1344        }
1345        return valid;
1346    }
1347
1348    private static class BetterCheckTask implements Callable<List<RemoteAddeEntry>> {
1349        private final RemoteAddeEntry entry;
1350        public BetterCheckTask(final RemoteAddeEntry entry) {
1351            this.entry = entry;
1352            this.entry.setEntryValidity(EntryValidity.VALIDATING);
1353        }
1354        @SuppressWarnings({"FeatureEnvy"})
1355        public List<RemoteAddeEntry> call() {
1356            List<RemoteAddeEntry> valid = arrList();
1357            if (RemoteAddeEntry.checkHost(entry)) {
1358                for (RemoteAddeEntry tmp : EntryTransforms.createEntriesFrom(entry)) {
1359                    if (RemoteAddeEntry.checkEntry(false, tmp) == AddeStatus.OK) {
1360                        tmp.setEntryValidity(EntryValidity.VERIFIED);
1361                        valid.add(tmp);
1362                    }
1363                }
1364            }
1365            if (!valid.isEmpty()) {
1366                entry.setEntryValidity(EntryValidity.VERIFIED);
1367            } else {
1368                entry.setEntryValidity(EntryValidity.INVALID);
1369            }
1370            return valid;
1371        }
1372    }
1373
1374    private class CheckEntryTask implements Callable<RemoteAddeEntry> {
1375        private final RemoteAddeEntry entry;
1376        public CheckEntryTask(final RemoteAddeEntry entry) {
1377            requireNonNull(entry);
1378            this.entry = entry;
1379            this.entry.setEntryValidity(EntryValidity.VALIDATING);
1380        }
1381        @SuppressWarnings({"FeatureEnvy"})
1382        public RemoteAddeEntry call() {
1383            AddeStatus status = RemoteAddeEntry.checkEntry(entry);
1384            switch (status) {
1385                case OK: entry.setEntryValidity(EntryValidity.VERIFIED); break;
1386                default: entry.setEntryValidity(EntryValidity.INVALID); break;
1387            }
1388            return entry;
1389        }
1390    }
1391
1392    private static class RemoteAddeTableModel extends AbstractTableModel {
1393
1394        // TODO(jon): these constants can go once things calm down
1395        private static final int VALID = 0;
1396        private static final int SOURCE = 1;
1397        private static final int DATASET = 2;
1398        private static final int ACCT = 3;
1399        private static final int TYPES = 4;
1400        private static final Pattern ENTRY_ID_SPLITTER = Pattern.compile("!");
1401
1402        /** Labels that appear as the column headers. */
1403        private final String[] columnNames = {
1404            "Valid", "Source", "Dataset", "Accounting", "Data Types"
1405        };
1406
1407        private final List<String> servers;
1408
1409        /** {@link EntryStore} used to query and apply changes. */
1410        private final EntryStore entryStore;
1411
1412        /**
1413         * Builds an {@link javax.swing.table.AbstractTableModel} with some extensions that
1414         * facilitate working with {@link RemoteAddeEntry RemoteAddeEntrys}.
1415         * 
1416         * @param entryStore Server manager object.
1417         */
1418        public RemoteAddeTableModel(final EntryStore entryStore) {
1419            requireNonNull(entryStore, "Cannot query a null EntryStore");
1420            this.entryStore = entryStore;
1421            this.servers = arrList(entryStore.getRemoteEntryTexts());
1422        }
1423
1424        /**
1425         * Returns the {@link RemoteAddeEntry} at the given index.
1426         * 
1427         * @param row Index of the entry.
1428         * 
1429         * @return The {@code RemoteAddeEntry} at the index specified by {@code row}.
1430         */
1431        protected List<RemoteAddeEntry> getEntriesAtRow(final int row) {
1432            String server = servers.get(row).replace('/', '!');
1433            List<RemoteAddeEntry> matches = arrList();
1434            for (AddeEntry entry : entryStore.searchWithPrefix(server)) {
1435                if (entry instanceof RemoteAddeEntry) {
1436                    matches.add((RemoteAddeEntry)entry);
1437                }
1438            }
1439            return matches;
1440        }
1441
1442        /**
1443         * Returns the index of the given {@code entry}.
1444         *
1445         * @param entry {@link RemoteAddeEntry} whose row is desired.
1446         *
1447         * @return Index of the desired {@code entry}, or {@code -1} if the
1448         * entry wasn't found.
1449         */
1450        protected int getRowForEntry(final RemoteAddeEntry entry) {
1451            return getRowForEntry(entry.getEntryText());
1452        }
1453
1454        /**
1455         * Returns the index of the given entry text within the table.
1456         *
1457         * @param entryText String representation of the desired entry.
1458         *
1459         * @return Index of the desired entry, or {@code -1} if the entry was
1460         * not found.
1461         *
1462         * @see edu.wisc.ssec.mcidasv.servermanager.AddeEntry#getEntryText()
1463         */
1464        protected int getRowForEntry(final String entryText) {
1465            return servers.indexOf(entryText);
1466        }
1467
1468        /**
1469         * Clears and re-adds all {@link RemoteAddeEntry}s within {@link #entryStore}.
1470         */
1471        public void refreshEntries() {
1472            servers.clear();
1473            servers.addAll(entryStore.getRemoteEntryTexts());
1474            this.fireTableDataChanged();
1475        }
1476
1477        /**
1478         * Returns the length of {@link #columnNames}.
1479         * 
1480         * @return The number of columns.
1481         */
1482        @Override public int getColumnCount() {
1483            return columnNames.length;
1484        }
1485
1486        /**
1487         * Returns the number of entries being managed.
1488         */
1489        @Override public int getRowCount() {
1490            return servers.size();
1491        }
1492
1493        /**
1494         * Finds the value at the given coordinates.
1495         * 
1496         * @param row Table row.
1497         * @param column Table column.
1498         * 
1499         * @return Value stored at the given {@code row} and {@code column}
1500         * coordinates
1501         * 
1502         * @throws IndexOutOfBoundsException if {@code row} or {@code column}
1503         * refer to an invalid table cell.
1504         */
1505        @Override public Object getValueAt(int row, int column) {
1506            String serverText = servers.get(row);
1507            String prefix = serverText.replace('/', '!');
1508            switch (column) {
1509                case VALID: return formattedValidity(prefix, entryStore);
1510                case SOURCE: return formattedSource(prefix, entryStore);
1511                case DATASET: return serverText;
1512                case ACCT: return formattedAccounting(prefix, entryStore);
1513                case TYPES: return formattedTypes(prefix, entryStore);
1514                default: throw new IndexOutOfBoundsException();
1515            }
1516        }
1517
1518        private static String formattedSource(final String serv, final EntryStore manager) {
1519            List<AddeEntry> matches = manager.searchWithPrefix(serv);
1520            EntrySource source = EntrySource.INVALID;
1521            if (!matches.isEmpty()) {
1522                for (AddeEntry entry : matches) {
1523                    if (entry.getEntrySource() == EntrySource.USER) {
1524                        return EntrySource.USER.toString();
1525                    }
1526                }
1527              source = matches.get(0).getEntrySource();
1528            }
1529            return source.toString();
1530        }
1531
1532        private static String formattedValidity(final String serv, final EntryStore manager) {
1533            List<AddeEntry> matches = manager.searchWithPrefix(serv);
1534            EntryValidity validity = EntryValidity.INVALID;
1535            if (!matches.isEmpty()) {
1536                validity = matches.get(0).getEntryValidity();
1537            }
1538            return validity.toString();
1539        }
1540
1541        private static String formattedAccounting(final String serv, final EntryStore manager) {
1542            List<AddeEntry> matches = manager.searchWithPrefix(serv);
1543            AddeAccount acct = AddeEntry.DEFAULT_ACCOUNT;
1544            if (!matches.isEmpty()) {
1545                acct = matches.get(0).getAccount();
1546            }
1547            if (AddeEntry.DEFAULT_ACCOUNT.equals(acct)) {
1548                return "public dataset";
1549            }
1550            return acct.friendlyString();
1551        }
1552
1553        private static boolean hasType(final String serv, final EntryStore manager, final EntryType type) {
1554            String[] chunks = ENTRY_ID_SPLITTER.split(serv);
1555            Set<EntryType> types = Collections.emptySet();
1556            if (chunks.length == 2) {
1557                types = manager.getTypes(chunks[0], chunks[1]);
1558            }
1559            return types.contains(type);
1560        }
1561
1562        private static String formattedTypes(final String serv, final EntryStore manager) {
1563            String[] chunks = ENTRY_ID_SPLITTER.split(serv);
1564            Set<EntryType> types = Collections.emptySet();
1565            if (chunks.length == 2) {
1566                types = manager.getTypes(chunks[0], chunks[1]);
1567            }
1568
1569            @SuppressWarnings({"MagicNumber"})
1570            StringBuilder sb = new StringBuilder(30);
1571            for (EntryType type : EnumSet.of(EntryType.IMAGE, EntryType.GRID, EntryType.NAV, EntryType.POINT, EntryType.RADAR, EntryType.TEXT)) {
1572                if (types.contains(type)) {
1573                    sb.append(type.toString()).append(' ');
1574                }
1575            }
1576            return sb.toString().toLowerCase();
1577        }
1578
1579        /**
1580         * Returns the column name associated with {@code column}.
1581         * 
1582         * @return One of {@link #columnNames}.
1583         */
1584        @Override public String getColumnName(final int column) {
1585            return columnNames[column];
1586        }
1587
1588        @Override public Class<?> getColumnClass(final int column) {
1589            return String.class;
1590        }
1591
1592        @Override public boolean isCellEditable(final int row, final int column) {
1593            return false;
1594        }
1595    }
1596
1597    private static class LocalAddeTableModel extends AbstractTableModel {
1598
1599        /** Labels that appear as the column headers. */
1600        private final String[] columnNames = {
1601            "Dataset (e.g. MYDATA)", "Image Type (e.g. JAN 07 GOES)", "Format", "Directory"
1602        };
1603
1604        /** Entries that currently populate the server manager. */
1605        private final List<LocalAddeEntry> entries;
1606
1607        /** {@link EntryStore} used to query and apply changes. */
1608        private final EntryStore entryStore;
1609
1610        public LocalAddeTableModel(final EntryStore entryStore) {
1611            requireNonNull(entryStore, "Cannot query a null EntryStore");
1612            this.entryStore = entryStore;
1613            this.entries = arrList(entryStore.getLocalEntries());
1614        }
1615
1616        /**
1617         * Returns the {@link LocalAddeEntry} at the given index.
1618         * 
1619         * @param row Index of the entry.
1620         * 
1621         * @return The {@code LocalAddeEntry} at the index specified by {@code row}.
1622         */
1623        protected LocalAddeEntry getEntryAtRow(final int row) {
1624            return entries.get(row);
1625        }
1626
1627        protected int getRowForEntry(final LocalAddeEntry entry) {
1628            return entries.indexOf(entry);
1629        }
1630
1631        protected List<LocalAddeEntry> getSelectedEntries(final int[] rows) {
1632            List<LocalAddeEntry> selected = arrList(rows.length);
1633            int rowCount = entries.size();
1634            for (int i = 0; i < rows.length; i++) {
1635                int tmpIdx = rows[i];
1636                if ((tmpIdx >= 0) && (tmpIdx < rowCount)) {
1637                    selected.add(entries.get(tmpIdx));
1638                } else {
1639                    throw new IndexOutOfBoundsException();
1640                }
1641            }
1642            return selected;
1643        }
1644
1645        public void refreshEntries() {
1646            entries.clear();
1647            entries.addAll(entryStore.getLocalEntries());
1648            this.fireTableDataChanged();
1649        }
1650
1651        /**
1652         * Returns the length of {@link #columnNames}.
1653         * 
1654         * @return The number of columns.
1655         */
1656        @Override public int getColumnCount() {
1657            return columnNames.length;
1658        }
1659
1660        /**
1661         * Returns the number of entries being managed.
1662         */
1663        @Override public int getRowCount() {
1664            return entries.size();
1665        }
1666
1667        /**
1668         * Finds the value at the given coordinates.
1669         * 
1670         * @param row Table row.
1671         * @param column Table column.
1672         * 
1673         * @return Value stored at the given {@code row} and {@code column}
1674         * coordinates
1675         *
1676         * @throws IndexOutOfBoundsException if {@code row} or {@code column}
1677         * refer to an invalid table cell.
1678         */
1679        @Override public Object getValueAt(int row, int column) {
1680            LocalAddeEntry entry = entries.get(row);
1681            if (entry == null) {
1682                throw new IndexOutOfBoundsException(); // still questionable...
1683            }
1684
1685            switch (column) {
1686                case 0: return entry.getGroup();
1687                case 1: return entry.getName();
1688                case 2: return entry.getFormat();
1689                case 3: return entry.getMask();
1690                default: throw new IndexOutOfBoundsException();
1691            }
1692        }
1693
1694        /**
1695         * Returns the column name associated with {@code column}.
1696         * 
1697         * @return One of {@link #columnNames}.
1698         */
1699        @Override public String getColumnName(final int column) {
1700            return columnNames[column];
1701        }
1702    }
1703
1704    // i need the following icons:
1705    // something to convey entry validity: invalid, verified, unverified
1706    // a "system" entry icon (thinking of something with prominent "V")
1707    // a "mctable" entry icon (similar to above, but with a prominent "X")
1708    // a "user" entry icon (no idea yet!)
1709    public class EntrySourceRenderer extends DefaultTableCellRenderer {
1710
1711        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
1712            Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
1713            EntrySource source = EntrySource.valueOf((String)value);
1714            EntrySourceRenderer renderer = (EntrySourceRenderer)comp;
1715            Icon icon = null;
1716            String tooltip = null;
1717            switch (source) {
1718                case SYSTEM:
1719                    icon = system;
1720                    tooltip = "Default dataset and cannot be removed, only disabled.";
1721                    break;
1722                case MCTABLE:
1723                    icon = mctable;
1724                    tooltip = "Dataset imported from a MCTABLE.TXT.";
1725                    break;
1726                case USER:
1727                    icon = user;
1728                    tooltip = "Dataset created or altered by you!";
1729                    break;
1730            }
1731            renderer.setIcon(icon);
1732            renderer.setToolTipText(tooltip);
1733            renderer.setText(null);
1734            return comp;
1735        }
1736    }
1737
1738    public class EntryValidityRenderer extends DefaultTableCellRenderer {
1739        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
1740            Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
1741            EntryValidity validity = EntryValidity.valueOf((String)value);
1742            EntryValidityRenderer renderer = (EntryValidityRenderer)comp;
1743            Icon icon = null;
1744            String msg = null;
1745            String tooltip = null;
1746            switch (validity) {
1747                case INVALID:
1748                    icon = invalid;
1749                    tooltip = "Dataset verification failed.";
1750                    break;
1751                case VERIFIED:
1752                    break;
1753                case UNVERIFIED:
1754                    icon = unverified;
1755                    tooltip = "Dataset has not been verified.";
1756                    break;
1757                case VALIDATING:
1758                    msg = "Checking...";
1759                    break;
1760            }
1761            renderer.setIcon(icon);
1762            renderer.setToolTipText(tooltip);
1763            renderer.setText(msg);
1764            return comp;
1765        }
1766    }
1767
1768    public static class TextRenderer extends DefaultTableCellRenderer {
1769
1770        /** */
1771        private Font bold;
1772
1773        /** */
1774        private Font boldItalic;
1775
1776        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
1777            Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
1778            Font currentFont = comp.getFont();
1779            if (bold == null) {
1780                bold = currentFont.deriveFont(Font.BOLD); 
1781            }
1782            if (boldItalic == null) {
1783                boldItalic = currentFont.deriveFont(Font.BOLD | Font.ITALIC);
1784            }
1785            if (column == 2) {
1786                comp.setFont(bold);
1787            } else if (column == 3) {
1788                // why can't i set the color for just a single column!?
1789            } else if (column == 4) {
1790                comp.setFont(boldItalic);
1791            }
1792            return comp;
1793        }
1794    }
1795
1796    /**
1797     * Construct an {@link Icon} object using the image at the specified
1798     * {@code path}.
1799     * 
1800     * @param path Path to image to use as an icon. Should not be {@code null}.
1801     * 
1802     * @return Icon object with the desired image.
1803     */
1804    private static Icon icon(final String path) {
1805        return GuiUtils.getImageIcon(path, TabbedAddeManager.class, true);
1806    }
1807
1808    /**
1809     * Launch the application. Makes for a simplistic test.
1810     * 
1811     * @param args Command line arguments. These are currently ignored.
1812     */
1813    public static void main(String[] args) {
1814        SwingUtilities.invokeLater(new Runnable() {
1815            public void run() {
1816                try {
1817                    TabbedAddeManager frame = new TabbedAddeManager();
1818                    frame.setVisible(true);
1819                } catch (Exception e) {
1820                    e.printStackTrace();
1821                }
1822            }
1823        });
1824    }
1825
1826    private JPanel contentPane;
1827    private JTable remoteTable;
1828    private JTable localTable;
1829    private JTabbedPane tabbedPane;
1830    private JLabel statusLabel;
1831    private JButton newRemoteButton;
1832    private JButton editRemoteButton;
1833    private JButton removeRemoteButton;
1834    private JButton importRemoteButton;
1835    private JButton newLocalButton;
1836    private JButton editLocalButton;
1837    private JButton removeLocalButton;
1838//    private JButton applyButton;
1839    private JButton okButton;
1840//    private JButton cancelButton;
1841    private JMenuItem editMenuItem;
1842    private JMenuItem removeMenuItem;
1843    private JCheckBox importAccountBox;
1844
1845    /** Icon for datasets that are part of a default McIDAS-V install. */
1846    private Icon system;
1847
1848    /** Icon for datasets that originate from a MCTABLE.TXT. */
1849    private Icon mctable;
1850
1851    /** Icon for datasets that the user has provided. */
1852    private Icon user;
1853
1854    /** Icon for invalid datasets. */
1855    private Icon invalid;
1856
1857    /** Icon for datasets that have not been verified. */
1858    private Icon unverified;
1859}