001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2015 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 */ 028package edu.wisc.ssec.mcidasv.servermanager; 029 030import static java.util.Objects.requireNonNull; 031 032import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList; 033import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashSet; 034import static edu.wisc.ssec.mcidasv.util.McVGuiUtils.runOnEDT; 035import static edu.wisc.ssec.mcidasv.util.McVGuiUtils.safeGetText; 036 037import java.awt.BorderLayout; 038import java.awt.Component; 039import java.awt.Dimension; 040import java.awt.Font; 041import java.awt.event.WindowAdapter; 042import java.awt.event.WindowEvent; 043import java.io.File; 044import java.util.Collection; 045import java.util.Collections; 046import java.util.EnumSet; 047import java.util.List; 048import java.util.Set; 049import java.util.concurrent.Callable; 050import java.util.concurrent.CompletionService; 051import java.util.concurrent.ExecutionException; 052import java.util.concurrent.ExecutorCompletionService; 053import java.util.concurrent.ExecutorService; 054import java.util.concurrent.Executors; 055import java.util.regex.Pattern; 056 057import javax.swing.Box; 058import javax.swing.BoxLayout; 059import javax.swing.GroupLayout; 060import javax.swing.Icon; 061import javax.swing.JButton; 062import javax.swing.JCheckBox; 063import javax.swing.JCheckBoxMenuItem; 064import javax.swing.JDialog; 065import javax.swing.JFileChooser; 066import javax.swing.JFrame; 067import javax.swing.JLabel; 068import javax.swing.JMenu; 069import javax.swing.JMenuBar; 070import javax.swing.JMenuItem; 071import javax.swing.JPanel; 072import javax.swing.JPopupMenu; 073import javax.swing.JScrollPane; 074import javax.swing.JSeparator; 075import javax.swing.JTabbedPane; 076import javax.swing.JTable; 077import javax.swing.JTextField; 078import javax.swing.LayoutStyle; 079import javax.swing.ListSelectionModel; 080import javax.swing.SwingUtilities; 081import javax.swing.UIManager; 082import javax.swing.WindowConstants; 083import javax.swing.border.EmptyBorder; 084import javax.swing.event.ChangeEvent; 085import javax.swing.event.ChangeListener; 086import javax.swing.event.ListSelectionEvent; 087import javax.swing.event.ListSelectionListener; 088import javax.swing.table.AbstractTableModel; 089import javax.swing.table.DefaultTableCellRenderer; 090 091import net.miginfocom.swing.MigLayout; 092 093import org.bushe.swing.event.EventBus; 094import org.bushe.swing.event.annotation.AnnotationProcessor; 095import org.bushe.swing.event.annotation.EventSubscriber; 096 097import org.slf4j.Logger; 098import org.slf4j.LoggerFactory; 099 100import ucar.unidata.idv.IdvObjectStore; 101import ucar.unidata.util.GuiUtils; 102import ucar.unidata.util.LogUtil; 103 104import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource; 105import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType; 106import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity; 107import edu.wisc.ssec.mcidasv.servermanager.AddeThread.McservEvent; 108import edu.wisc.ssec.mcidasv.servermanager.EntryStore.Event; 109import edu.wisc.ssec.mcidasv.servermanager.RemoteEntryEditor.AddeStatus; 110import edu.wisc.ssec.mcidasv.ui.BetterJTable; 111import edu.wisc.ssec.mcidasv.util.McVTextField.Prompt; 112import java.awt.event.ActionListener; 113import 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"}) 123public 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 requireNonNull(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 * handle {@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(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(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(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(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(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(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(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(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 = safeGetText(importUser); 1221 if (forceUser.isEmpty()) { 1222 forceUser = AddeEntry.DEFAULT_ACCOUNT.getUsername(); 1223 defaultUser = true; 1224 } 1225 1226 boolean defaultProj = false; 1227 String forceProj = safeGetText(importProject); 1228 if (forceProj.isEmpty()) { 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 requireNonNull(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 requireNonNull(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 requireNonNull(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 requireNonNull(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 * Construct an {@link Icon} object using the image at the specified 1798 * {@code path}. 1799 * 1800 * @param path Path to image to use as an icon. Should not be {@code null}. 1801 * 1802 * @return Icon object with the desired image. 1803 */ 1804 private static Icon icon(final String path) { 1805 return GuiUtils.getImageIcon(path, TabbedAddeManager.class, true); 1806 } 1807 1808 /** 1809 * Launch the application. Makes for a simplistic test. 1810 * 1811 * @param args Command line arguments. These are currently ignored. 1812 */ 1813 public static void main(String[] args) { 1814 SwingUtilities.invokeLater(new Runnable() { 1815 public void run() { 1816 try { 1817 TabbedAddeManager frame = new TabbedAddeManager(); 1818 frame.setVisible(true); 1819 } catch (Exception e) { 1820 e.printStackTrace(); 1821 } 1822 } 1823 }); 1824 } 1825 1826 private JPanel contentPane; 1827 private JTable remoteTable; 1828 private JTable localTable; 1829 private JTabbedPane tabbedPane; 1830 private JLabel statusLabel; 1831 private JButton newRemoteButton; 1832 private JButton editRemoteButton; 1833 private JButton removeRemoteButton; 1834 private JButton importRemoteButton; 1835 private JButton newLocalButton; 1836 private JButton editLocalButton; 1837 private JButton removeLocalButton; 1838// private JButton applyButton; 1839 private JButton okButton; 1840// private JButton cancelButton; 1841 private JMenuItem editMenuItem; 1842 private JMenuItem removeMenuItem; 1843 private JCheckBox importAccountBox; 1844 1845 /** Icon for datasets that are part of a default McIDAS-V install. */ 1846 private Icon system; 1847 1848 /** Icon for datasets that originate from a MCTABLE.TXT. */ 1849 private Icon mctable; 1850 1851 /** Icon for datasets that the user has provided. */ 1852 private Icon user; 1853 1854 /** Icon for invalid datasets. */ 1855 private Icon invalid; 1856 1857 /** Icon for datasets that have not been verified. */ 1858 private Icon unverified; 1859}