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