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