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 }