001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2016 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.setColumnSelectionAllowed(false); 620 remoteTable.setRowSelectionAllowed(true); 621 remoteTable.getTableHeader().setReorderingAllowed(false); 622 remoteTable.setFont(UIManager.getFont("Table.font").deriveFont(11.0f)); 623 remoteTable.getColumnModel().getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 624 remoteTable.setDefaultRenderer(String.class, new TextRenderer()); 625 remoteTable.getColumnModel().getColumn(0).setPreferredWidth(10); 626 remoteTable.getColumnModel().getColumn(1).setPreferredWidth(10); 627 remoteTable.getColumnModel().getColumn(3).setPreferredWidth(50); 628 remoteTable.getColumnModel().getColumn(4).setPreferredWidth(50); 629 remoteTable.getColumnModel().getColumn(0).setCellRenderer(new EntryValidityRenderer()); 630 remoteTable.getColumnModel().getColumn(1).setCellRenderer(new EntrySourceRenderer()); 631 remoteTable.getSelectionModel().addListSelectionListener(this::remoteSelectionModelChanged); 632 remoteTable.addMouseListener(new MouseAdapter() { 633 @Override public void mouseClicked(final MouseEvent e) { 634 if ((e.getClickCount() == 2) && hasSingleRemoteSelection()) { 635 showRemoteEditor(getSelectedRemoteEntries()); 636 } 637 } 638 }); 639 remoteScroller.setViewportView(remoteTable); 640 remoteTab.add(remoteScroller); 641 642 JPanel remoteActionPanel = new JPanel(); 643 remoteTab.add(remoteActionPanel); 644 remoteActionPanel.setLayout(new BoxLayout(remoteActionPanel, BoxLayout.X_AXIS)); 645 646 newRemoteButton = new JButton("Add New Dataset"); 647 newRemoteButton.addActionListener(e -> showRemoteEditor()); 648 newRemoteButton.setToolTipText("Create a new remote ADDE dataset."); 649 remoteActionPanel.add(newRemoteButton); 650 651 editRemoteButton = new JButton("Edit Dataset"); 652 editRemoteButton.addActionListener(e -> showRemoteEditor(getSelectedRemoteEntries())); 653 editRemoteButton.setToolTipText("Edit an existing remote ADDE dataset."); 654 remoteActionPanel.add(editRemoteButton); 655 656 removeRemoteButton = new JButton("Remove Selection"); 657 removeRemoteButton.addActionListener(e -> removeRemoteEntries(getSelectedRemoteEntries())); 658 removeRemoteButton.setToolTipText("Remove the selected remote ADDE datasets."); 659 remoteActionPanel.add(removeRemoteButton); 660 661 importRemoteButton = new JButton("Import MCTABLE..."); 662 importRemoteButton.addActionListener(e -> importButtonActionPerformed(e)); 663 remoteActionPanel.add(importRemoteButton); 664 665 JPanel localTab = new JPanel(); 666 localTab.setBorder(new EmptyBorder(0, 4, 4, 4)); 667 tabbedPane.addTab("Local Data", null, localTab, null); 668 localTab.setLayout(new BoxLayout(localTab, BoxLayout.Y_AXIS)); 669 670 localTable = new BetterJTable(); 671 JScrollPane localScroller = 672 BetterJTable.createStripedJScrollPane(localTable); 673 localTable.setModel(new LocalAddeTableModel(serverManager)); 674 localTable.setColumnSelectionAllowed(false); 675 localTable.setRowSelectionAllowed(true); 676 localTable.getTableHeader().setReorderingAllowed(false); 677 localTable.setFont(UIManager.getFont("Table.font").deriveFont(11.0f)); 678 localTable.setDefaultRenderer(String.class, new TextRenderer()); 679 localTable.getColumnModel().getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 680 localTable.getSelectionModel().addListSelectionListener(this::localSelectionModelChanged); 681 localTable.addMouseListener(new MouseAdapter() { 682 @Override public void mouseClicked(final MouseEvent e) { 683 if ((e.getClickCount() == 2) && hasSingleLocalSelection()) { 684 showLocalEditor(getSingleLocalSelection()); 685 } 686 } 687 }); 688 localScroller.setViewportView(localTable); 689 localTab.add(localScroller); 690 691 JPanel localActionPanel = new JPanel(); 692 localTab.add(localActionPanel); 693 localActionPanel.setLayout(new BoxLayout(localActionPanel, BoxLayout.X_AXIS)); 694 695 newLocalButton = new JButton("Add New Dataset"); 696 newLocalButton.addActionListener(e -> showLocalEditor()); 697 newLocalButton.setToolTipText("Create a new local ADDE dataset."); 698 localActionPanel.add(newLocalButton); 699 700 editLocalButton = new JButton("Edit Dataset"); 701 editLocalButton.setEnabled(false); 702 editLocalButton.addActionListener(e -> showLocalEditor(getSingleLocalSelection())); 703 editLocalButton.setToolTipText("Edit an existing local ADDE dataset."); 704 localActionPanel.add(editLocalButton); 705 706 removeLocalButton = new JButton("Remove Selection"); 707 removeLocalButton.setEnabled(false); 708 removeLocalButton.addActionListener(e -> removeLocalEntries(getSelectedLocalEntries())); 709 removeLocalButton.setToolTipText("Remove the selected local ADDE datasets."); 710 localActionPanel.add(removeLocalButton); 711 712 JComponent statusPanel = new JPanel(); 713 statusPanel.setBorder(new EmptyBorder(0, 6, 0, 6)); 714 contentPane.add(statusPanel, "cell 0 3,grow"); 715 statusPanel.setLayout(new BorderLayout(0, 0)); 716 717 Box statusMessageBox = Box.createHorizontalBox(); 718 statusPanel.add(statusMessageBox, BorderLayout.WEST); 719 720 String statusMessage = McservEvent.STOPPED.getMessage(); 721 if (serverManager.checkLocalServer()) { 722 statusMessage = McservEvent.ACTIVE.getMessage(); 723 } 724 statusLabel = new JLabel(statusMessage); 725 statusMessageBox.add(statusLabel); 726 statusLabel.setEnabled(false); 727 728 Box frameControlBox = Box.createHorizontalBox(); 729 statusPanel.add(frameControlBox, BorderLayout.EAST); 730 731 okButton = new JButton("Ok"); 732 okButton.addActionListener(e -> closeManager()); 733 frameControlBox.add(okButton); 734 tabbedPane.setSelectedIndex(getLastTab()); 735 guiInitialized = true; 736 } 737 738 /** 739 * Respond to changes in {@link #tabbedPane}; primarily switching tabs. 740 * 741 * @param event Event being handled. Ignored for now. 742 */ 743 private void handleTabStateChanged(final ChangeEvent event) { 744 assert SwingUtilities.isEventDispatchThread(); 745 boolean hasSelection = false; 746 int index = 0; 747 if (guiInitialized) { 748 index = tabbedPane.getSelectedIndex(); 749 if (index == 0) { 750 hasSelection = hasRemoteSelection(); 751 editRemoteButton.setEnabled(hasSelection); 752 removeRemoteButton.setEnabled(hasSelection); 753 } else { 754 hasSelection = hasLocalSelection(); 755 editLocalButton.setEnabled(hasSelection); 756 removeLocalButton.setEnabled(hasSelection); 757 } 758 editMenuItem.setEnabled(hasSelection); 759 removeMenuItem.setEnabled(hasSelection); 760 setLastTab(index); 761 } 762 logger.trace("index={} hasRemote={} hasLocal={} guiInit={}", new Object[] {index, hasRemoteSelection(), hasLocalSelection(), guiInitialized}); 763 } 764 765 /** 766 * Respond to events. 767 * 768 * @param e {@link ListSelectionEvent} that necessitated this call. 769 */ 770 private void remoteSelectionModelChanged(final ListSelectionEvent e) { 771 if (e.getValueIsAdjusting()) { 772 return; 773 } 774 775 int selectedRowCount = 0; 776 ListSelectionModel selModel = (ListSelectionModel)e.getSource(); 777 Set<RemoteAddeEntry> selectedEntries; 778 if (selModel.isSelectionEmpty()) { 779 selectedEntries = Collections.emptySet(); 780 } else { 781 int min = selModel.getMinSelectionIndex(); 782 int max = selModel.getMaxSelectionIndex(); 783 RemoteAddeTableModel tableModel = (RemoteAddeTableModel)remoteTable.getModel(); 784 selectedEntries = newLinkedHashSet((max - min) * AddeEntry.EntryType.values().length); 785 for (int i = min; i <= max; i++) { 786 if (selModel.isSelectedIndex(i)) { 787 List<RemoteAddeEntry> entries = tableModel.getEntriesAtRow(i); 788 selectedEntries.addAll(entries); 789 selectedRowCount++; 790 } 791 } 792 } 793 794 boolean onlyDefaultEntries = true; 795 for (RemoteAddeEntry entry : selectedEntries) { 796 if (entry.getEntrySource() != EntrySource.SYSTEM) { 797 onlyDefaultEntries = false; 798 break; 799 } 800 } 801 setSelectedRemoteEntries(selectedEntries); 802 803 // the current "edit" dialog doesn't work so well with multiple 804 // servers/datasets, so only allow the user to edit entries one at a time. 805 boolean singleSelection = selectedRowCount == 1; 806 editRemoteButton.setEnabled(singleSelection); 807 editMenuItem.setEnabled(singleSelection); 808 809 boolean hasSelection = (selectedRowCount >= 1) && !onlyDefaultEntries; 810 removeRemoteButton.setEnabled(hasSelection); 811 removeMenuItem.setEnabled(hasSelection); 812 } 813 814 /** 815 * Respond to events from the local dataset table. 816 * 817 * @param e {@link ListSelectionEvent} that necessitated this call. 818 */ 819 private void localSelectionModelChanged(final ListSelectionEvent e) { 820 if (e.getValueIsAdjusting()) { 821 return; 822 } 823 ListSelectionModel selModel = (ListSelectionModel)e.getSource(); 824 Set<LocalAddeEntry> selectedEntries; 825 if (selModel.isSelectionEmpty()) { 826 selectedEntries = Collections.emptySet(); 827 } else { 828 int min = selModel.getMinSelectionIndex(); 829 int max = selModel.getMaxSelectionIndex(); 830 LocalAddeTableModel tableModel = (LocalAddeTableModel)localTable.getModel(); 831 selectedEntries = newLinkedHashSet(max - min); 832 for (int i = min; i <= max; i++) { 833 if (selModel.isSelectedIndex(i)) { 834 selectedEntries.add(tableModel.getEntryAtRow(i)); 835 } 836 } 837 } 838 839 setSelectedLocalEntries(selectedEntries); 840 841 // the current "edit" dialog doesn't work so well with multiple 842 // servers/datasets, so only allow the user to edit entries one at a time. 843 boolean singleSelection = selectedEntries.size() == 1; 844 this.editRemoteButton.setEnabled(singleSelection); 845 this.editMenuItem.setEnabled(singleSelection); 846 847 boolean hasSelection = !selectedEntries.isEmpty(); 848 removeRemoteButton.setEnabled(hasSelection); 849 removeMenuItem.setEnabled(hasSelection); 850 } 851 852 /** 853 * Checks to see if {@link #selectedRemoteEntries} contains any 854 * {@link RemoteAddeEntry}s. 855 * 856 * @return Whether or not any {@code RemoteAddeEntry} values are selected. 857 */ 858 private boolean hasRemoteSelection() { 859 return !selectedRemoteEntries.isEmpty(); 860 } 861 862 /** 863 * Checks to see if {@link #selectedLocalEntries} contains any 864 * {@link LocalAddeEntry}s. 865 * 866 * @return Whether or not any {@code LocalAddeEntry} values are selected. 867 */ 868 private boolean hasLocalSelection() { 869 return !selectedLocalEntries.isEmpty(); 870 } 871 872 /** 873 * Checks to see if the user has select a <b>single</b> remote dataset. 874 * 875 * @return {@code true} if there is a single remote dataset selected. 876 * {@code false} otherwise. 877 */ 878 private boolean hasSingleRemoteSelection() { 879 String entryText = null; 880 boolean result = true; 881 for (RemoteAddeEntry entry : selectedRemoteEntries) { 882 if (entryText == null) { 883 entryText = entry.getEntryText(); 884 } 885 if (!entry.getEntryText().equals(entryText)) { 886 result = false; 887 break; 888 } 889 } 890 return result; 891 } 892 893 /** 894 * Checks to see if the user has select a <b>single</b> local dataset. 895 * 896 * @return {@code true} if there is a single local dataset selected. {@code false} otherwise. 897 */ 898 private boolean hasSingleLocalSelection() { 899 return selectedLocalEntries.size() == 1; 900 } 901 902 /** 903 * If there is a single local dataset selected, this method will return that 904 * dataset. 905 * 906 * @return Either the single selected local dataset, or {@link LocalAddeEntry#INVALID_ENTRY}. 907 */ 908 private LocalAddeEntry getSingleLocalSelection() { 909 LocalAddeEntry entry = LocalAddeEntry.INVALID_ENTRY; 910 if (selectedLocalEntries.size() == 1) { 911 entry = selectedLocalEntries.get(0); 912 } 913 return entry; 914 } 915 916 /** 917 * Corresponds to the selected remote ADDE entries in the GUI. 918 * 919 * @param entries Should not be {@code null}. 920 */ 921 private void setSelectedRemoteEntries(final Collection<RemoteAddeEntry> entries) { 922 selectedRemoteEntries.clear(); 923 selectedRemoteEntries.addAll(entries); 924 this.editRemoteButton.setEnabled(entries.size() == 1); 925 this.removeRemoteButton.setEnabled(!entries.isEmpty()); 926 logger.trace("remote entries={}", entries); 927 } 928 929 /** 930 * Gets the selected remote ADDE entries. 931 * 932 * @return Either an empty list or the remote entries selected in the GUI. 933 */ 934 private List<RemoteAddeEntry> getSelectedRemoteEntries() { 935 List<RemoteAddeEntry> selected = Collections.emptyList(); 936 if (!selectedRemoteEntries.isEmpty()) { 937 selected = arrList(selectedRemoteEntries); 938 } 939 return selected; 940 } 941 942 /** 943 * Corresponds to the selected local ADDE entries in the GUI. 944 * 945 * @param entries Should not be {@code null}. 946 */ 947 private void setSelectedLocalEntries(final Collection<LocalAddeEntry> entries) { 948 selectedLocalEntries.clear(); 949 selectedLocalEntries.addAll(entries); 950 this.editLocalButton.setEnabled(entries.size() == 1); 951 this.removeLocalButton.setEnabled(!entries.isEmpty()); 952 logger.trace("local entries={}", entries); 953 } 954 955 /** 956 * Gets the selected local ADDE entries. 957 * 958 * @return Either an empty list or the local entries selected in the GUI. 959 */ 960 private List<LocalAddeEntry> getSelectedLocalEntries() { 961 List<LocalAddeEntry> selected = Collections.emptyList(); 962 if (!selectedLocalEntries.isEmpty()) { 963 selected = arrList(selectedLocalEntries); 964 } 965 return selected; 966 } 967 968 /** 969 * Handles the user closing the server manager GUI. 970 * 971 * @param evt Event that triggered this method call. Currently ignored. 972 * 973 * @see #closeManager() 974 */ 975 private void formWindowClosed(WindowEvent evt) { 976 closeManager(); 977 } 978 979 @SuppressWarnings({"MagicNumber"}) 980 private JPanel makeFileChooserAccessory() { 981 assert SwingUtilities.isEventDispatchThread(); 982 JPanel accessory = new JPanel(); 983 accessory.setLayout(new BoxLayout(accessory, BoxLayout.PAGE_AXIS)); 984 importAccountBox = new JCheckBox("Use ADDE Accounting?"); 985 importAccountBox.setSelected(false); 986 importAccountBox.addActionListener(evt -> { 987 boolean selected = importAccountBox.isSelected(); 988 importUser.setEnabled(selected); 989 importProject.setEnabled(selected); 990 }); 991 String clientProp = "JComponent.sizeVariant"; 992 String propVal = "mini"; 993 994 importUser = new JTextField(); 995 importUser.putClientProperty(clientProp, propVal); 996 Prompt userPrompt = new Prompt(importUser, "Username"); 997 userPrompt.putClientProperty(clientProp, propVal); 998 importUser.setEnabled(importAccountBox.isSelected()); 999 1000 importProject = new JTextField(); 1001 Prompt projPrompt = new Prompt(importProject, "Project Number"); 1002 projPrompt.putClientProperty(clientProp, propVal); 1003 importProject.putClientProperty(clientProp, propVal); 1004 importProject.setEnabled(importAccountBox.isSelected()); 1005 1006 GroupLayout layout = new GroupLayout(accessory); 1007 accessory.setLayout(layout); 1008 layout.setHorizontalGroup( 1009 layout.createParallelGroup(GroupLayout.Alignment.LEADING) 1010 .addComponent(importAccountBox) 1011 .addGroup(layout.createSequentialGroup() 1012 .addContainerGap() 1013 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING, false) 1014 .addComponent(importProject, GroupLayout.Alignment.LEADING) 1015 .addComponent(importUser, GroupLayout.Alignment.LEADING, GroupLayout.DEFAULT_SIZE, 131, Short.MAX_VALUE))) 1016 ); 1017 layout.setVerticalGroup( 1018 layout.createParallelGroup(GroupLayout.Alignment.LEADING) 1019 .addGroup(layout.createSequentialGroup() 1020 .addComponent(importAccountBox) 1021 .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) 1022 .addComponent(importUser, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) 1023 .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) 1024 .addComponent(importProject, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) 1025 .addContainerGap(55, (int)Short.MAX_VALUE)) 1026 ); 1027 return accessory; 1028 } 1029 1030 private void importButtonActionPerformed(ActionEvent evt) { 1031 assert SwingUtilities.isEventDispatchThread(); 1032 JFileChooser fc = new JFileChooser(getLastImportPath()); 1033 fc.setAccessory(makeFileChooserAccessory()); 1034 fc.setFileSelectionMode(JFileChooser.FILES_ONLY); 1035 int ret = fc.showOpenDialog(this); 1036 if (ret == JFileChooser.APPROVE_OPTION) { 1037 File f = fc.getSelectedFile(); 1038 String path = f.getPath(); 1039 1040 boolean defaultUser = false; 1041 String forceUser = safeGetText(importUser); 1042 if (forceUser.isEmpty()) { 1043 forceUser = AddeEntry.DEFAULT_ACCOUNT.getUsername(); 1044 defaultUser = true; 1045 } 1046 1047 boolean defaultProj = false; 1048 String forceProj = safeGetText(importProject); 1049 if (forceProj.isEmpty()) { 1050 forceProj = AddeEntry.DEFAULT_ACCOUNT.getProject(); 1051 defaultProj = true; 1052 } 1053 1054 if (importAccountBox.isSelected() && (defaultUser || defaultProj)) { 1055 logger.warn("bad acct dialog: forceUser={} forceProj={}", forceUser, forceProj); 1056 } else { 1057 logger.warn("acct appears valid: forceUser={} forceProj={}", forceUser, forceProj); 1058 importMctable(path, forceUser, forceProj); 1059 // don't worry about file validity; i'll just assume the user clicked 1060 // on the wrong entry by accident. 1061 setLastImportPath(f.getParent()); 1062 } 1063 } 1064 } 1065 1066 /** 1067 * Returns the directory that contained the most recently imported MCTABLE.TXT. 1068 * 1069 * @return Either the path to the most recently imported MCTABLE.TXT file, 1070 * or an empty {@code String}. 1071 */ 1072 private String getLastImportPath() { 1073 String lastPath = serverManager.getIdvStore().get(LAST_IMPORTED, ""); 1074 logger.trace("last path='{}'", lastPath); 1075 return lastPath; 1076 } 1077 1078 /** 1079 * Saves the directory that contained the most recently imported MCTABLE.TXT. 1080 * 1081 * @param path Path to the most recently imported MCTABLE.TXT file. 1082 * {@code null} values are replaced with an empty {@code String}. 1083 */ 1084 private void setLastImportPath(final String path) { 1085 String okayPath = (path == null) ? "" : path; 1086 logger.trace("saving path='{}'", okayPath); 1087 serverManager.getIdvStore().put(LAST_IMPORTED, okayPath); 1088 } 1089 1090 /** 1091 * Returns the index of the user's last server manager tab. 1092 * 1093 * @return Index of the user's most recently viewed server manager tab, or {@code 0}. 1094 */ 1095 private int getLastTab() { 1096 int index = serverManager.getIdvStore().get(LAST_TAB, 0); 1097 logger.trace("last tab={}", index); 1098 return index; 1099 } 1100 1101 /** 1102 * Saves the index of the last server manager tab the user was looking at. 1103 * 1104 * @param index Index of the user's most recently viewed server manager tab. 1105 */ 1106 private void setLastTab(final int index) { 1107 int okayIndex = ((index >= 0) && (index < 2)) ? index : 0; 1108 IdvObjectStore store = serverManager.getIdvStore(); 1109 logger.trace("storing tab={}", okayIndex); 1110 store.put(LAST_TAB, okayIndex); 1111 } 1112 1113 // stupid adde.ucar.edu entries never seem to time out! great! making the gui hang is just so awesome! 1114 @SuppressWarnings({"ObjectAllocationInLoop"}) 1115 public Set<RemoteAddeEntry> checkDatasets(final Collection<RemoteAddeEntry> entries) { 1116 requireNonNull(entries, "can't check a null collection of entries"); 1117 if (entries.isEmpty()) { 1118 return Collections.emptySet(); 1119 } 1120 1121 Set<RemoteAddeEntry> valid = newLinkedHashSet(); 1122 ExecutorService exec = Executors.newFixedThreadPool(POOL); 1123 CompletionService<List<RemoteAddeEntry>> ecs = 1124 new ExecutorCompletionService<>(exec); 1125 final RemoteAddeTableModel tableModel = 1126 (RemoteAddeTableModel)remoteTable.getModel(); 1127 1128 // place entries 1129 for (RemoteAddeEntry entry : entries) { 1130 ecs.submit(new BetterCheckTask(entry)); 1131 logger.trace("submitting entry={}", entry); 1132 final int row = tableModel.getRowForEntry(entry); 1133 runOnEDT(() -> tableModel.fireTableRowsUpdated(row, row)); 1134 } 1135 1136 // work through the entries 1137 try { 1138 for (int i = 0; i < entries.size(); i++) { 1139 final List<RemoteAddeEntry> checkedEntries = ecs.take().get(); 1140 if (!checkedEntries.isEmpty()) { 1141 final int row = 1142 tableModel.getRowForEntry(checkedEntries.get(0)); 1143 runOnEDT(() -> { 1144 List<RemoteAddeEntry> old = 1145 tableModel.getEntriesAtRow(row); 1146 serverManager.replaceEntries(old, checkedEntries); 1147 tableModel.fireTableRowsUpdated(row, row); 1148 }); 1149 } 1150 valid.addAll(checkedEntries); 1151 } 1152 } catch (InterruptedException e) { 1153 LogUtil.logException("Interrupted while validating entries", e); 1154 } catch (ExecutionException e) { 1155 LogUtil.logException("ADDE validation execution error", e); 1156 } finally { 1157 exec.shutdown(); 1158 } 1159 return valid; 1160 } 1161 1162 private static class BetterCheckTask implements Callable<List<RemoteAddeEntry>> { 1163 private final RemoteAddeEntry entry; 1164 public BetterCheckTask(final RemoteAddeEntry entry) { 1165 this.entry = entry; 1166 this.entry.setEntryValidity(EntryValidity.VALIDATING); 1167 } 1168 @SuppressWarnings({"FeatureEnvy"}) 1169 public List<RemoteAddeEntry> call() { 1170 List<RemoteAddeEntry> valid = arrList(); 1171 if (RemoteAddeEntry.checkHost(entry)) { 1172 EntryTransforms.createEntriesFrom(entry) 1173 .stream() 1174 .filter(tmp -> RemoteAddeEntry.checkEntry(false, tmp) == AddeStatus.OK) 1175 .forEach(tmp -> { 1176 tmp.setEntryValidity(EntryValidity.VERIFIED); 1177 valid.add(tmp); 1178 }); 1179 } 1180 if (valid.isEmpty()) { 1181 entry.setEntryValidity(EntryValidity.INVALID); 1182 } else { 1183 entry.setEntryValidity(EntryValidity.VERIFIED); 1184 } 1185 return valid; 1186 } 1187 } 1188 1189 private class CheckEntryTask implements Callable<RemoteAddeEntry> { 1190 private final RemoteAddeEntry entry; 1191 public CheckEntryTask(final RemoteAddeEntry entry) { 1192 requireNonNull(entry); 1193 this.entry = entry; 1194 this.entry.setEntryValidity(EntryValidity.VALIDATING); 1195 } 1196 @SuppressWarnings({"FeatureEnvy"}) 1197 public RemoteAddeEntry call() { 1198 AddeStatus status = RemoteAddeEntry.checkEntry(entry); 1199 switch (status) { 1200 case OK: entry.setEntryValidity(EntryValidity.VERIFIED); break; 1201 default: entry.setEntryValidity(EntryValidity.INVALID); break; 1202 } 1203 return entry; 1204 } 1205 } 1206 1207 private static class RemoteAddeTableModel extends AbstractTableModel { 1208 1209 // TODO(jon): these constants can go once things calm down 1210 private static final int VALID = 0; 1211 private static final int SOURCE = 1; 1212 private static final int DATASET = 2; 1213 private static final int ACCT = 3; 1214 private static final int TYPES = 4; 1215 private static final Pattern ENTRY_ID_SPLITTER = Pattern.compile("!"); 1216 1217 /** Labels that appear as the column headers. */ 1218 private final String[] columnNames = { 1219 "Valid", "Source", "Dataset", "Accounting", "Data Types" 1220 }; 1221 1222 private final List<String> servers; 1223 1224 /** {@link EntryStore} used to query and apply changes. */ 1225 private final EntryStore entryStore; 1226 1227 /** 1228 * Builds an {@link javax.swing.table.AbstractTableModel} with some 1229 * extensions that facilitate working with 1230 * {@link RemoteAddeEntry RemoteAddeEntrys}. 1231 * 1232 * @param entryStore Server manager object. 1233 */ 1234 public RemoteAddeTableModel(final EntryStore entryStore) { 1235 requireNonNull(entryStore, "Cannot query a null EntryStore"); 1236 this.entryStore = entryStore; 1237 this.servers = arrList(entryStore.getRemoteEntryTexts()); 1238 } 1239 1240 /** 1241 * Returns the {@link RemoteAddeEntry} at the given index. 1242 * 1243 * @param row Index of the entry. 1244 * 1245 * @return {@code RemoteAddeEntry} at index specified by {@code row}. 1246 */ 1247 protected List<RemoteAddeEntry> getEntriesAtRow(final int row) { 1248 String server = servers.get(row).replace('/', '!'); 1249 List<RemoteAddeEntry> matches = arrList(); 1250 matches.addAll( 1251 entryStore.searchWithPrefix(server) 1252 .stream() 1253 .filter(entry -> entry instanceof RemoteAddeEntry) 1254 .map(entry -> (RemoteAddeEntry) entry) 1255 .collect(Collectors.toList())); 1256 return matches; 1257 } 1258 1259 /** 1260 * Returns the index of the given {@code entry}. 1261 * 1262 * @param entry {@link RemoteAddeEntry} whose row is desired. 1263 * 1264 * @return Index of the desired {@code entry}, or {@code -1} if the 1265 * entry wasn't found. 1266 */ 1267 protected int getRowForEntry(final RemoteAddeEntry entry) { 1268 return getRowForEntry(entry.getEntryText()); 1269 } 1270 1271 /** 1272 * Returns the index of the given entry text within the table. 1273 * 1274 * @param entryText String representation of the desired entry. 1275 * 1276 * @return Index of the desired entry, or {@code -1} if the entry was 1277 * not found. 1278 * 1279 * @see AddeEntry#getEntryText() 1280 */ 1281 protected int getRowForEntry(final String entryText) { 1282 return servers.indexOf(entryText); 1283 } 1284 1285 /** 1286 * Clears and re-adds all {@link RemoteAddeEntry}s within 1287 * {@link #entryStore}. 1288 */ 1289 public void refreshEntries() { 1290 servers.clear(); 1291 servers.addAll(entryStore.getRemoteEntryTexts()); 1292 this.fireTableDataChanged(); 1293 } 1294 1295 /** 1296 * Returns the length of {@link #columnNames}. 1297 * 1298 * @return The number of columns. 1299 */ 1300 @Override public int getColumnCount() { 1301 return columnNames.length; 1302 } 1303 1304 /** 1305 * Returns the number of entries being managed. 1306 */ 1307 @Override public int getRowCount() { 1308 return servers.size(); 1309 } 1310 1311 /** 1312 * Finds the value at the given coordinates. 1313 * 1314 * @param row Table row. 1315 * @param column Table column. 1316 * 1317 * @return Value stored at the given {@code row} and {@code column} 1318 * coordinates 1319 * 1320 * @throws IndexOutOfBoundsException if {@code row} or {@code column} 1321 * refer to an invalid table cell. 1322 */ 1323 @Override public Object getValueAt(int row, int column) { 1324 String serverText = servers.get(row); 1325 String prefix = serverText.replace('/', '!'); 1326 switch (column) { 1327 case VALID: return formattedValidity(prefix, entryStore); 1328 case SOURCE: return formattedSource(prefix, entryStore); 1329 case DATASET: return serverText; 1330 case ACCT: return formattedAccounting(prefix, entryStore); 1331 case TYPES: return formattedTypes(prefix, entryStore); 1332 default: throw new IndexOutOfBoundsException(); 1333 } 1334 } 1335 1336 private static String formattedSource(final String serv, 1337 final EntryStore manager) 1338 { 1339 List<AddeEntry> matches = manager.searchWithPrefix(serv); 1340 EntrySource source = EntrySource.INVALID; 1341 if (!matches.isEmpty()) { 1342 for (AddeEntry entry : matches) { 1343 if (entry.getEntrySource() == EntrySource.USER) { 1344 return EntrySource.USER.toString(); 1345 } 1346 } 1347 source = matches.get(0).getEntrySource(); 1348 } 1349 return source.toString(); 1350 } 1351 1352 private static String formattedValidity(final String serv, 1353 final EntryStore manager) 1354 { 1355 List<AddeEntry> matches = manager.searchWithPrefix(serv); 1356 EntryValidity validity = EntryValidity.INVALID; 1357 if (!matches.isEmpty()) { 1358 validity = matches.get(0).getEntryValidity(); 1359 } 1360 return validity.toString(); 1361 } 1362 1363 private static String formattedAccounting(final String serv, 1364 final EntryStore manager) 1365 { 1366 List<AddeEntry> matches = manager.searchWithPrefix(serv); 1367 AddeAccount acct = AddeEntry.DEFAULT_ACCOUNT; 1368 if (!matches.isEmpty()) { 1369 acct = matches.get(0).getAccount(); 1370 } 1371 if (AddeEntry.DEFAULT_ACCOUNT.equals(acct)) { 1372 return "public dataset"; 1373 } 1374 return acct.friendlyString(); 1375 } 1376 1377 private static boolean hasType(final String serv, 1378 final EntryStore manager, 1379 final EntryType type) 1380 { 1381 String[] chunks = ENTRY_ID_SPLITTER.split(serv); 1382 Set<EntryType> types = Collections.emptySet(); 1383 if (chunks.length == 2) { 1384 types = manager.getTypes(chunks[0], chunks[1]); 1385 } 1386 return types.contains(type); 1387 } 1388 1389 private static String formattedTypes(final String serv, 1390 final EntryStore manager) 1391 { 1392 String[] chunks = ENTRY_ID_SPLITTER.split(serv); 1393// Set<EntryType> types = Collections.emptySet(); 1394// if (chunks.length == 2) { 1395// types = manager.getTypes(chunks[0], chunks[1]); 1396// } 1397 Set<EntryType> types = chunks.length == 2 1398 ? manager.getTypes(chunks[0], chunks[1]) 1399 : Collections.emptySet(); 1400 1401 1402// @SuppressWarnings({"MagicNumber"}) 1403// StringBuilder sb = new StringBuilder(30); 1404// for (EntryType type : EnumSet.of(EntryType.IMAGE, EntryType.GRID, EntryType.NAV, EntryType.POINT, EntryType.RADAR, EntryType.TEXT)) { 1405// if (types.contains(type)) { 1406// sb.append(type.toString()).append(' '); 1407// } 1408// } 1409 return EnumSet.of(EntryType.IMAGE, EntryType.GRID, EntryType.NAV, 1410 EntryType.POINT, EntryType.RADAR, EntryType.TEXT 1411 ).stream() 1412 .filter(types::contains) 1413 .map(String::valueOf) 1414 .collect(Collectors.joining(" ")); 1415// return sb.toString().toLowerCase(); 1416 } 1417 1418 /** 1419 * Returns the column name associated with {@code column}. 1420 * 1421 * @return One of {@link #columnNames}. 1422 */ 1423 @Override public String getColumnName(final int column) { 1424 return columnNames[column]; 1425 } 1426 1427 @Override public Class<?> getColumnClass(final int column) { 1428 return String.class; 1429 } 1430 1431 @Override public boolean isCellEditable(final int row, 1432 final int column) 1433 { 1434 return false; 1435 } 1436 } 1437 1438 private static class LocalAddeTableModel extends AbstractTableModel { 1439 1440 /** Labels that appear as the column headers. */ 1441 private final String[] columnNames = { 1442 "Dataset (e.g. MYDATA)", "Image Type (e.g. JAN 07 GOES)", 1443 "Format", "Directory" 1444 }; 1445 1446 /** Entries that currently populate the server manager. */ 1447 private final List<LocalAddeEntry> entries; 1448 1449 /** {@link EntryStore} used to query and apply changes. */ 1450 private final EntryStore entryStore; 1451 1452 public LocalAddeTableModel(final EntryStore entryStore) { 1453 requireNonNull(entryStore, "Cannot query a null EntryStore"); 1454 this.entryStore = entryStore; 1455 this.entries = arrList(entryStore.getLocalEntries()); 1456 } 1457 1458 /** 1459 * Returns the {@link LocalAddeEntry} at the given index. 1460 * 1461 * @param row Index of the entry. 1462 * 1463 * @return {@code LocalAddeEntry} at index specified by {@code row}. 1464 */ 1465 protected LocalAddeEntry getEntryAtRow(final int row) { 1466 return entries.get(row); 1467 } 1468 1469 protected int getRowForEntry(final LocalAddeEntry entry) { 1470 return entries.indexOf(entry); 1471 } 1472 1473 protected List<LocalAddeEntry> getSelectedEntries(final int[] rows) { 1474 List<LocalAddeEntry> selected = arrList(rows.length); 1475 int rowCount = entries.size(); 1476 for (int tmpIdx : rows) { 1477 if ((tmpIdx >= 0) && (tmpIdx < rowCount)) { 1478 selected.add(entries.get(tmpIdx)); 1479 } else { 1480 throw new IndexOutOfBoundsException(); 1481 } 1482 } 1483 return selected; 1484 } 1485 1486 public void refreshEntries() { 1487 entries.clear(); 1488 entries.addAll(entryStore.getLocalEntries()); 1489 this.fireTableDataChanged(); 1490 } 1491 1492 /** 1493 * Returns the length of {@link #columnNames}. 1494 * 1495 * @return The number of columns. 1496 */ 1497 @Override public int getColumnCount() { 1498 return columnNames.length; 1499 } 1500 1501 /** 1502 * Returns the number of entries being managed. 1503 */ 1504 @Override public int getRowCount() { 1505 return entries.size(); 1506 } 1507 1508 /** 1509 * Finds the value at the given coordinates. 1510 * 1511 * @param row Table row. 1512 * @param column Table column. 1513 * 1514 * @return Value stored at the given {@code row} and {@code column} 1515 * coordinates 1516 * 1517 * @throws IndexOutOfBoundsException if {@code row} or {@code column} 1518 * refer to an invalid table cell. 1519 */ 1520 @Override public Object getValueAt(int row, int column) { 1521 LocalAddeEntry entry = entries.get(row); 1522 if (entry == null) { 1523 throw new IndexOutOfBoundsException(); // still questionable... 1524 } 1525 1526 switch (column) { 1527 case 0: return entry.getGroup(); 1528 case 1: return entry.getName(); 1529 case 2: return entry.getFormat(); 1530 case 3: return entry.getMask(); 1531 default: throw new IndexOutOfBoundsException(); 1532 } 1533 } 1534 1535 /** 1536 * Returns the column name associated with {@code column}. 1537 * 1538 * @return One of {@link #columnNames}. 1539 */ 1540 @Override public String getColumnName(final int column) { 1541 return columnNames[column]; 1542 } 1543 } 1544 1545 // i need the following icons: 1546 // something to convey entry validity: invalid, verified, unverified 1547 // a "system" entry icon (thinking of something with prominent "V") 1548 // a "mctable" entry icon (similar to above, but with a prominent "X") 1549 // a "user" entry icon (no idea yet!) 1550 public class EntrySourceRenderer extends DefaultTableCellRenderer { 1551 1552 public Component getTableCellRendererComponent(JTable table, 1553 Object value, 1554 boolean isSelected, 1555 boolean hasFocus, 1556 int row, 1557 int column) 1558 { 1559 Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 1560 EntrySource source = EntrySource.valueOf((String)value); 1561 EntrySourceRenderer renderer = (EntrySourceRenderer)comp; 1562 Icon icon = null; 1563 String tooltip = null; 1564 switch (source) { 1565 case SYSTEM: 1566 icon = system; 1567 tooltip = "Default dataset and cannot be removed, only disabled."; 1568 break; 1569 case MCTABLE: 1570 icon = mctable; 1571 tooltip = "Dataset imported from a MCTABLE.TXT."; 1572 break; 1573 case USER: 1574 icon = user; 1575 tooltip = "Dataset created or altered by you!"; 1576 break; 1577 } 1578 renderer.setIcon(icon); 1579 renderer.setToolTipText(tooltip); 1580 renderer.setText(null); 1581 return comp; 1582 } 1583 } 1584 1585 public class EntryValidityRenderer extends DefaultTableCellRenderer { 1586 public Component getTableCellRendererComponent(JTable table, 1587 Object value, 1588 boolean isSelected, 1589 boolean hasFocus, 1590 int row, 1591 int column) 1592 { 1593 Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 1594 EntryValidity validity = EntryValidity.valueOf((String)value); 1595 EntryValidityRenderer renderer = (EntryValidityRenderer)comp; 1596 Icon icon = null; 1597 String msg = null; 1598 String tooltip = null; 1599 switch (validity) { 1600 case INVALID: 1601 icon = invalid; 1602 tooltip = "Dataset verification failed."; 1603 break; 1604 case VERIFIED: 1605 break; 1606 case UNVERIFIED: 1607 icon = unverified; 1608 tooltip = "Dataset has not been verified."; 1609 break; 1610 case VALIDATING: 1611 msg = "Checking..."; 1612 break; 1613 } 1614 renderer.setIcon(icon); 1615 renderer.setToolTipText(tooltip); 1616 renderer.setText(msg); 1617 return comp; 1618 } 1619 } 1620 1621 public static class TextRenderer extends DefaultTableCellRenderer { 1622 1623 /** */ 1624 private Font bold; 1625 1626 /** */ 1627 private Font boldItalic; 1628 1629 public Component getTableCellRendererComponent(JTable table, 1630 Object value, 1631 boolean isSelected, 1632 boolean hasFocus, 1633 int row, 1634 int column) 1635 { 1636 Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 1637 Font currentFont = comp.getFont(); 1638 if (bold == null) { 1639 bold = currentFont.deriveFont(Font.BOLD); 1640 } 1641 if (boldItalic == null) { 1642 boldItalic = currentFont.deriveFont(Font.BOLD | Font.ITALIC); 1643 } 1644 if (column == 2) { 1645 comp.setFont(bold); 1646 } else if (column == 3) { 1647 // why can't i set the color for just a single column!? 1648 } else if (column == 4) { 1649 comp.setFont(boldItalic); 1650 } 1651 return comp; 1652 } 1653 } 1654 1655 /** 1656 * Construct an {@link Icon} object using the image at the specified 1657 * {@code path}. 1658 * 1659 * @param path Path to image to use as an icon. Should not be {@code null}. 1660 * 1661 * @return Icon object with the desired image. 1662 */ 1663 private static Icon icon(final String path) { 1664 return GuiUtils.getImageIcon(path, TabbedAddeManager.class, true); 1665 } 1666 1667 /** 1668 * Launch the application. Makes for a simplistic test. 1669 * 1670 * @param args Command line arguments. These are currently ignored. 1671 */ 1672 public static void main(String[] args) { 1673 SwingUtilities.invokeLater(() -> { 1674 try { 1675 TabbedAddeManager frame = new TabbedAddeManager(); 1676 frame.setVisible(true); 1677 } catch (Exception e) { 1678 e.printStackTrace(); 1679 } 1680 }); 1681 } 1682 1683 private JPanel contentPane; 1684 private JTable remoteTable; 1685 private JTable localTable; 1686 private JTabbedPane tabbedPane; 1687 private JLabel statusLabel; 1688 private JButton newRemoteButton; 1689 private JButton editRemoteButton; 1690 private JButton removeRemoteButton; 1691 private JButton importRemoteButton; 1692 private JButton newLocalButton; 1693 private JButton editLocalButton; 1694 private JButton removeLocalButton; 1695 private JButton okButton; 1696 private JMenuItem editMenuItem; 1697 private JMenuItem removeMenuItem; 1698 private JCheckBox importAccountBox; 1699 1700 /** Icon for datasets that are part of a default McIDAS-V install. */ 1701 private Icon system; 1702 1703 /** Icon for datasets that originate from a MCTABLE.TXT. */ 1704 private Icon mctable; 1705 1706 /** Icon for datasets that the user has provided. */ 1707 private Icon user; 1708 1709 /** Icon for invalid datasets. */ 1710 private Icon invalid; 1711 1712 /** Icon for datasets that have not been verified. */ 1713 private Icon unverified; 1714}