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