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