001    /*
002     * $Id: TabbedAddeManager.java,v 1.53 2012/04/16 15:54:32 jbeavers Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
007     * Space Science and Engineering Center (SSEC)
008     * University of Wisconsin - Madison
009     * 1225 W. Dayton Street, Madison, WI 53706, USA
010     * https://www.ssec.wisc.edu/mcidas
011     * 
012     * All Rights Reserved
013     * 
014     * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015     * some McIDAS-V source code is based on IDV and VisAD source code.  
016     * 
017     * McIDAS-V is free software; you can redistribute it and/or modify
018     * it under the terms of the GNU Lesser Public License as published by
019     * the Free Software Foundation; either version 3 of the License, or
020     * (at your option) any later version.
021     * 
022     * McIDAS-V is distributed in the hope that it will be useful,
023     * but WITHOUT ANY WARRANTY; without even the implied warranty of
024     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025     * GNU Lesser Public License for more details.
026     * 
027     * You should have received a copy of the GNU Lesser Public License
028     * along with this program.  If not, see http://www.gnu.org/licenses.
029     */
030    package edu.wisc.ssec.mcidasv.servermanager;
031    
032    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
033    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashSet;
034    import static edu.wisc.ssec.mcidasv.util.Contract.notNull;
035    import static edu.wisc.ssec.mcidasv.util.McVGuiUtils.runOnEDT;
036    
037    import java.awt.BorderLayout;
038    import java.awt.Component;
039    import java.awt.Dimension;
040    import java.awt.Font;
041    import java.awt.event.WindowAdapter;
042    import java.awt.event.WindowEvent;
043    import java.io.File;
044    import java.util.Collection;
045    import java.util.Collections;
046    import java.util.EnumSet;
047    import java.util.List;
048    import java.util.Set;
049    import java.util.concurrent.Callable;
050    import java.util.concurrent.CompletionService;
051    import java.util.concurrent.ExecutionException;
052    import java.util.concurrent.ExecutorCompletionService;
053    import java.util.concurrent.ExecutorService;
054    import java.util.concurrent.Executors;
055    import java.util.regex.Pattern;
056    
057    import javax.swing.Box;
058    import javax.swing.BoxLayout;
059    import javax.swing.GroupLayout;
060    import javax.swing.Icon;
061    import javax.swing.JButton;
062    import javax.swing.JCheckBox;
063    import javax.swing.JCheckBoxMenuItem;
064    import javax.swing.JDialog;
065    import javax.swing.JFileChooser;
066    import javax.swing.JFrame;
067    import javax.swing.JLabel;
068    import javax.swing.JMenu;
069    import javax.swing.JMenuBar;
070    import javax.swing.JMenuItem;
071    import javax.swing.JPanel;
072    import javax.swing.JPopupMenu;
073    import javax.swing.JScrollPane;
074    import javax.swing.JSeparator;
075    import javax.swing.JTabbedPane;
076    import javax.swing.JTable;
077    import javax.swing.JTextField;
078    import javax.swing.LayoutStyle;
079    import javax.swing.ListSelectionModel;
080    import javax.swing.SwingUtilities;
081    import javax.swing.UIManager;
082    import javax.swing.WindowConstants;
083    import javax.swing.border.EmptyBorder;
084    import javax.swing.event.ChangeEvent;
085    import javax.swing.event.ChangeListener;
086    import javax.swing.event.ListSelectionEvent;
087    import javax.swing.event.ListSelectionListener;
088    import javax.swing.table.AbstractTableModel;
089    import javax.swing.table.DefaultTableCellRenderer;
090    
091    import net.miginfocom.swing.MigLayout;
092    
093    import org.bushe.swing.event.EventBus;
094    import org.bushe.swing.event.annotation.AnnotationProcessor;
095    import org.bushe.swing.event.annotation.EventSubscriber;
096    
097    import org.slf4j.Logger;
098    import org.slf4j.LoggerFactory;
099    
100    import ucar.unidata.idv.IdvObjectStore;
101    import ucar.unidata.util.GuiUtils;
102    import ucar.unidata.util.LogUtil;
103    
104    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource;
105    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
106    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity;
107    import edu.wisc.ssec.mcidasv.servermanager.AddeThread.McservEvent;
108    import edu.wisc.ssec.mcidasv.servermanager.EntryStore.Event;
109    import edu.wisc.ssec.mcidasv.servermanager.RemoteEntryEditor.AddeStatus;
110    import edu.wisc.ssec.mcidasv.ui.BetterJTable;
111    import edu.wisc.ssec.mcidasv.util.McVTextField.Prompt;
112    import java.awt.event.ActionListener;
113    import 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"})
123    public 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            notNull(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         * handled {@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(java.awt.event.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(java.awt.event.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(java.awt.event.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(java.awt.event.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(java.awt.event.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(java.awt.event.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(java.awt.event.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(java.awt.event.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 = importUser.getText();
1221                if (forceUser.length() == 0) {
1222                    forceUser = AddeEntry.DEFAULT_ACCOUNT.getUsername();
1223                    defaultUser = true;
1224                }
1225    
1226                boolean defaultProj = false;
1227                String forceProj = importProject.getText();
1228                if (forceProj.length() == 0) {
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            notNull(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                notNull(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                notNull(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                notNull(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         * 
1798         * 
1799         * @param path
1800         * 
1801         * @return
1802         */
1803        private static Icon icon(final String path) {
1804            return GuiUtils.getImageIcon(path, TabbedAddeManager.class, true);
1805        }
1806    
1807        /**
1808         * Launch the application. Makes for a simplistic test.
1809         * 
1810         * @param args Command line arguments. These are currently ignored.
1811         */
1812        public static void main(String[] args) {
1813            SwingUtilities.invokeLater(new Runnable() {
1814                public void run() {
1815                    try {
1816                        TabbedAddeManager frame = new TabbedAddeManager();
1817                        frame.setVisible(true);
1818                    } catch (Exception e) {
1819                        e.printStackTrace();
1820                    }
1821                }
1822            });
1823        }
1824    
1825        private JPanel contentPane;
1826        private JTable remoteTable;
1827        private JTable localTable;
1828        private JTabbedPane tabbedPane;
1829        private JLabel statusLabel;
1830        private JButton newRemoteButton;
1831        private JButton editRemoteButton;
1832        private JButton removeRemoteButton;
1833        private JButton importRemoteButton;
1834        private JButton newLocalButton;
1835        private JButton editLocalButton;
1836        private JButton removeLocalButton;
1837    //    private JButton applyButton;
1838        private JButton okButton;
1839    //    private JButton cancelButton;
1840        private JMenuItem editMenuItem;
1841        private JMenuItem removeMenuItem;
1842        private JCheckBox importAccountBox;
1843    
1844        /** Icon for datasets that are part of a default McIDAS-V install. */
1845        private Icon system;
1846    
1847        /** Icon for datasets that originate from a MCTABLE.TXT. */
1848        private Icon mctable;
1849    
1850        /** Icon for datasets that the user has provided. */
1851        private Icon user;
1852    
1853        /** Icon for invalid datasets. */
1854        private Icon invalid;
1855    
1856        /** Icon for datasets that have not been verified. */
1857        private Icon unverified;
1858    }