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