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 java.util.Objects.requireNonNull; 031 032import static javax.swing.GroupLayout.DEFAULT_SIZE; 033import static javax.swing.GroupLayout.PREFERRED_SIZE; 034import static javax.swing.GroupLayout.Alignment.BASELINE; 035import static javax.swing.GroupLayout.Alignment.LEADING; 036import static javax.swing.GroupLayout.Alignment.TRAILING; 037import static javax.swing.LayoutStyle.ComponentPlacement.RELATED; 038import static javax.swing.LayoutStyle.ComponentPlacement.UNRELATED; 039 040import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashSet; 041import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newMap; 042import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.set; 043import static edu.wisc.ssec.mcidasv.util.McVGuiUtils.runOnEDT; 044import static edu.wisc.ssec.mcidasv.util.McVGuiUtils.safeGetText; 045 046import java.awt.Color; 047import java.awt.Frame; 048import java.awt.event.ActionEvent; 049import java.awt.event.ActionListener; 050import java.awt.event.WindowEvent; 051import java.util.Collection; 052import java.util.Collections; 053import java.util.EnumSet; 054import java.util.LinkedHashSet; 055import java.util.LinkedHashMap; 056import java.util.List; 057import java.util.Map; 058import java.util.Set; 059import java.util.StringTokenizer; 060import java.util.concurrent.Callable; 061import java.util.concurrent.CompletionService; 062import java.util.concurrent.ExecutionException; 063import java.util.concurrent.ExecutorCompletionService; 064import java.util.concurrent.ExecutorService; 065import java.util.concurrent.Executors; 066import java.util.concurrent.Future; 067import java.util.concurrent.TimeUnit; 068import java.util.stream.Collectors; 069 070import javax.swing.BorderFactory; 071import javax.swing.GroupLayout; 072import javax.swing.JButton; 073import javax.swing.JCheckBox; 074import javax.swing.JDialog; 075import javax.swing.JLabel; 076import javax.swing.JPanel; 077import javax.swing.JTextField; 078import javax.swing.SwingUtilities; 079import javax.swing.WindowConstants; 080import javax.swing.event.DocumentEvent; 081import javax.swing.event.DocumentListener; 082 083import org.slf4j.Logger; 084import org.slf4j.LoggerFactory; 085 086import ucar.unidata.util.LogUtil; 087 088import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EditorAction; 089import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource; 090import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType; 091import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity; 092import edu.wisc.ssec.mcidasv.util.CollectionHelpers; 093import edu.wisc.ssec.mcidasv.util.McVTextField; 094 095/** 096 * Simple dialog that allows the user to define or modify 097 * {@link RemoteAddeEntry RemoteAddeEntries}. 098 */ 099@SuppressWarnings("serial") 100public class RemoteEntryEditor extends JDialog { 101 102 /** Logger object. */ 103 private static final Logger logger = 104 LoggerFactory.getLogger(RemoteEntryEditor.class); 105 106 /** Possible entry verification states. */ 107 public enum AddeStatus { 108 PREFLIGHT, BAD_SERVER, BAD_ACCOUNTING, NO_METADATA, OK, BAD_GROUP 109 } 110 111 /** Number of threads in the thread pool. */ 112 private static final int POOL = 5; 113 114 /** 115 * Whether or not to input in the dataset, username, and project fields 116 * should be uppercased. 117 */ 118 private static final String PREF_FORCE_CAPS = "mcv.servers.forcecaps"; 119 120 /** 121 * Background {@link java.awt.Color Color} of an {@literal "invalid"} 122 * {@link JTextField}. 123 */ 124 private static final Color ERROR_FIELD_COLOR = Color.PINK; 125 126 /** 127 * Text {@link java.awt.Color Color} of an {@literal "invalid"} 128 * {@link JTextField}. 129 * */ 130 private static final Color ERROR_TEXT_COLOR = Color.WHITE; 131 132 /** 133 * Background {@link java.awt.Color Color} of a {@literal "valid"} 134 * {@link JTextField}. 135 * */ 136 private static final Color NORMAL_FIELD_COLOR = Color.WHITE; 137 138 /** 139 * Text {@link java.awt.Color Color} of a {@literal "valid"} 140 * {@link JTextField}. 141 */ 142 private static final Color NORMAL_TEXT_COLOR = Color.BLACK; 143 144 /** 145 * Contains any {@code JTextField}s that may be in an invalid 146 * (to McIDAS-V) state. 147 */ 148 private final Set<JTextField> badFields = newLinkedHashSet(25); 149 150 /** Reference back to the server manager. */ 151 private final EntryStore entryStore; 152 153 /** 154 * Allows for asynchronous verification of ADDE entries. 155 * May be {@code null}. 156 */ 157 private ExecutorService exec; 158 159 /** Current contents of the editor. */ 160 private final Set<RemoteAddeEntry> currentEntries = newLinkedHashSet(); 161 162 /** The last dialog action performed by the user. */ 163 private EditorAction editorAction = EditorAction.INVALID; 164 165 /** 166 * Initial contents of {@link #serverField}. 167 * Be aware that {@code null} is allowed. 168 */ 169 private final String serverText; 170 171 /** 172 * Initial contents of {@link #datasetField}. 173 * Be aware that {@code null} is allowed. 174 */ 175 private final String datasetText; 176 177 /** Whether or not the editor is prompting the user to adjust input. */ 178 private boolean inErrorState = false; 179 180 // if we decide to restore error overlays for known "bad" values. 181// private Set<RemoteAddeEntry> invalidEntries = CollectionHelpers.newLinkedHashSet(); 182 183 /** 184 * Populates the server and dataset text fields with given {@link String}s. 185 * This only works if the dialog <b>is not yet visible</b>. 186 * 187 * <p>This is mostly useful when adding an entry from a chooser. 188 * 189 * @param entryStore Reference to the server manager. 190 * @param address Should be the address of a server, but empty and 191 * {@code null} values are allowed. 192 * @param group Should be the name of a group/dataset on {@code server}, 193 * but empty and {@code null} values are allowed. 194 */ 195 public RemoteEntryEditor(EntryStore entryStore, String address, String group) { 196 super((JDialog)null, true); 197 this.entryStore = entryStore; 198// this.manager = null; 199 this.serverText = address; 200 this.datasetText = group; 201 initComponents(RemoteAddeEntry.INVALID_ENTRIES); 202 } 203 204 // TODO(jon): hold back on javadocs, this is likely to change 205 public RemoteEntryEditor(Frame parent, boolean modal, final TabbedAddeManager manager, final EntryStore store) { 206 this(parent, modal, manager, store, RemoteAddeEntry.INVALID_ENTRIES); 207 } 208 209 public RemoteEntryEditor(Frame parent, boolean modal, final TabbedAddeManager manager, final EntryStore store, final RemoteAddeEntry entry) { 210 this(parent, modal, manager, store, CollectionHelpers.list(entry)); 211 } 212 213 // TODO(jon): hold back on javadocs, this is likely to change 214 public RemoteEntryEditor(Frame parent, boolean modal, final TabbedAddeManager manager, final EntryStore store, final List<RemoteAddeEntry> entries) { 215 super(manager, modal); 216 this.entryStore = store; 217// this.manager = manager; 218 this.serverText = null; 219 this.datasetText = null; 220 if (! entries.equals(RemoteAddeEntry.INVALID_ENTRIES)) { 221 currentEntries.addAll(entries); 222 } 223 initComponents(entries); 224 } 225 226 /** 227 * Poll the various UI components and attempt to construct valid ADDE 228 * entries based upon the information provided by the user. 229 * 230 * @param ignoreCheckboxes Whether or not the {@literal "type"} checkboxes 231 * should get ignored. Setting this to {@code true} means that <i>all</i> 232 * types are considered valid--which is useful when attempting to verify 233 * the user's input. 234 * 235 * @return {@link Set} of entries that represent the user's input, or an 236 * empty {@code Set} if the input was invalid somehow. 237 */ 238 private Set<RemoteAddeEntry> pollWidgets(final boolean ignoreCheckboxes) { 239 String host = safeGetText(serverField).trim(); 240 String dataset = safeGetText(datasetField).trim(); 241 String username = RemoteAddeEntry.DEFAULT_ACCOUNT.getUsername(); 242 String project = RemoteAddeEntry.DEFAULT_ACCOUNT.getProject(); 243 if (acctBox.isSelected()) { 244 username = safeGetText(userField).trim(); 245 project = safeGetText(projField).trim(); 246 } 247 248 // determine the "valid" types 249 Set<EntryType> selectedTypes = newLinkedHashSet(); 250 if (!ignoreCheckboxes) { 251 if (imageBox.isSelected()) { 252 selectedTypes.add(EntryType.IMAGE); 253 } 254 if (pointBox.isSelected()) { 255 selectedTypes.add(EntryType.POINT); 256 } 257 if (gridBox.isSelected()) { 258 selectedTypes.add(EntryType.GRID); 259 } 260 if (textBox.isSelected()) { 261 selectedTypes.add(EntryType.TEXT); 262 } 263 if (navBox.isSelected()) { 264 selectedTypes.add(EntryType.NAV); 265 } 266 if (radarBox.isSelected()) { 267 selectedTypes.add(EntryType.RADAR); 268 } 269 } else { 270 selectedTypes.addAll(set(EntryType.IMAGE, EntryType.POINT, EntryType.GRID, EntryType.TEXT, EntryType.NAV, EntryType.RADAR)); 271 } 272 273 if (selectedTypes.isEmpty()) { 274 selectedTypes.add(EntryType.UNKNOWN); 275 } 276 277 // deal with the user trying to add multiple groups at once (even though this UI doesn't work right with it) 278 StringTokenizer tok = new StringTokenizer(dataset, ","); 279 Set<String> newDatasets = newLinkedHashSet(); 280 while (tok.hasMoreTokens()) { 281 newDatasets.add(tok.nextToken().trim()); 282 } 283 284 // create a new entry for each group and its valid types. 285 Set<RemoteAddeEntry> entries = newLinkedHashSet(); 286 for (String newGroup : newDatasets) { 287 for (EntryType type : selectedTypes) { 288 RemoteAddeEntry.Builder builder = new RemoteAddeEntry.Builder(host, newGroup).type(type).validity(EntryValidity.VERIFIED).source(EntrySource.USER); 289 if (acctBox.isSelected()) { 290 builder = builder.account(username, project); 291 } 292 RemoteAddeEntry newEntry = builder.build(); 293 List<AddeEntry> matches = entryStore.searchWithPrefix(newEntry.asStringId()); 294 if (matches.isEmpty()) { 295 entries.add(newEntry); 296 } else if (matches.size() == 1) { 297 AddeEntry matchedEntry = matches.get(0); 298 if (matchedEntry.getEntrySource() != EntrySource.SYSTEM) { 299 entries.add(newEntry); 300 } else { 301 entries.add((RemoteAddeEntry)matchedEntry); 302 } 303 } else { 304 // results should only be empty or a single entry 305 logger.warn("server manager returned unexpected results={}", matches); 306 } 307 } 308 } 309 return entries; 310 } 311 312 private void disposeDisplayable(final boolean refreshManager) { 313 if (isDisplayable()) { 314 dispose(); 315 } 316 TabbedAddeManager tmpController = TabbedAddeManager.getTabbedManager(); 317 if (refreshManager && (tmpController != null)) { 318 tmpController.refreshDisplay(); 319 } 320 } 321 322 /** 323 * Creates new {@link RemoteAddeEntry}s based upon the contents of the dialog 324 * and adds {@literal "them"} to the managed servers. If the dialog is 325 * displayed, we call {@link #dispose()} and attempt to refresh the 326 * server manager GUI if it is available. 327 */ 328 private void addEntry() { 329 Set<RemoteAddeEntry> addedEntries = pollWidgets(false); 330 entryStore.addEntries(addedEntries); 331// if (manager != null) { 332// manager.addEntries(addedEntries); 333// } 334 disposeDisplayable(true); 335 } 336 337 /** 338 * Replaces the entries within {@link #currentEntries} with new entries 339 * from {@link #pollWidgets(boolean)}. If the dialog is displayed, we call 340 * {@link #dispose()} and attempt to refresh the server manager GUI if it's 341 * available. 342 */ 343 private void editEntry() { 344 Set<RemoteAddeEntry> newEntries = pollWidgets(false); 345 entryStore.replaceEntries(currentEntries, newEntries); 346// if (manager != null) { 347// manager.replaceEntries(currentEntries, newEntries); 348// } 349 logger.trace("currentEntries={}", currentEntries); 350 disposeDisplayable(true); 351 } 352 353 /** 354 * Attempts to verify that the current contents of the GUI are 355 * {@literal "valid"}. 356 */ 357 private void verifyInput(final EditorAction action) { 358 resetBadFields(); 359 Set<RemoteAddeEntry> unverifiedEntries = pollWidgets(true); 360 361 // the editor GUI only works with one server address at a time. so 362 // although there may be several RemoteAddeEntry objs, they'll all have 363 // the same address and the following *isn't* as dumb as it looks! 364 if (!unverifiedEntries.isEmpty()) { 365 if (!RemoteAddeEntry.checkHost(unverifiedEntries.toArray(new RemoteAddeEntry[0])[0])) { 366 setStatus("Could not connect to the given server."); 367 setBadField(serverField, true); 368 return; 369 } 370 } else { 371 setStatus("Please specify "); 372 setBadField(serverField, true); 373 return; 374 } 375 376 setStatus("Contacting server..."); 377 Thread checkThread = makeCheckThread(action, unverifiedEntries); 378 checkThread.start(); 379 } 380 381 /** 382 * Displays a short status message in {@link #statusLabel}. 383 * 384 * @param msg Status message. Shouldn't be {@code null}. 385 */ 386 private void setStatus(final String msg) { 387 assert msg != null; 388 logger.debug("msg={}", msg); 389 runOnEDT(() -> statusLabel.setText(msg)); 390 statusLabel.revalidate(); 391 } 392 393 /** 394 * Marks a {@code JTextField} as {@literal "valid"} or {@literal "invalid"}. 395 * Mostly this just means that the field is highlighted in order to provide 396 * to the user a sense of {@literal "what do I fix"} when something goes 397 * wrong. 398 * 399 * @param field {@code JTextField} to mark. 400 * @param isBad {@code true} means that the field is {@literal "invalid"}, 401 * {@code false} means that the field is {@literal "valid"}. 402 */ 403 private void setBadField(final JTextField field, final boolean isBad) { 404 assert field != null; 405 assert field == serverField || field == datasetField || field == userField || field == projField; 406 407 if (isBad) { 408 badFields.add(field); 409 } else { 410 badFields.remove(field); 411 } 412 413 runOnEDT(() -> { 414 if (isBad) { 415 field.setForeground(ERROR_TEXT_COLOR); 416 field.setBackground(ERROR_FIELD_COLOR); 417 } else { 418 field.setForeground(NORMAL_TEXT_COLOR); 419 field.setBackground(NORMAL_FIELD_COLOR); 420 } 421 }); 422 field.revalidate(); 423 } 424 425 /** 426 * Determines whether or not any fields are in an invalid state. Useful 427 * for disallowing the user to add invalid entries to the server manager. 428 * 429 * @return Whether or not any fields are invalid. 430 */ 431 private boolean anyBadFields() { 432 assert badFields != null; 433 return !badFields.isEmpty(); 434 } 435 436 /** 437 * Clear out {@link #badFields} and {@literal "set"} the field's status to 438 * valid. 439 */ 440 private void resetBadFields() { 441 Set<JTextField> fields = new LinkedHashSet<>(badFields); 442 for (JTextField field : fields) { 443 setBadField(field, false); 444 } 445 } 446 447 /** 448 * Returns the last {@link EditorAction} that was performed. 449 * 450 * @return Last editor action performed. 451 * 452 * @see #editorAction 453 */ 454 public EditorAction getEditorAction() { 455 return editorAction; 456 } 457 458 /** 459 * Set the {@link EditorAction} that was performed. 460 * 461 * @param editorAction Action that was performed. 462 * 463 * @see #editorAction 464 */ 465 private void setEditorAction(final EditorAction editorAction) { 466 this.editorAction = editorAction; 467 } 468 469 /** 470 * Controls the value associated with the {@link #PREF_FORCE_CAPS} preference. 471 * 472 * @param value {@code true} causes user input into the dataset, username, 473 * and project fields to be capitalized. 474 * 475 * @see #getForceMcxCaps() 476 */ 477 private void setForceMcxCaps(final boolean value) { 478 entryStore.getIdvStore().put(PREF_FORCE_CAPS, value); 479 } 480 481 /** 482 * Returns the value associated with the {@link #PREF_FORCE_CAPS} preference. 483 * 484 * @return Whether or not user input should be automatically capitalized. 485 * 486 * @see #setForceMcxCaps(boolean) 487 */ 488 private boolean getForceMcxCaps() { 489 return entryStore.getIdvStore().get(PREF_FORCE_CAPS, true); 490 } 491 492 // TODO(jon): oh man clean this junk up 493 /** This method is called from within the constructor to 494 * initialize the form. 495 * WARNING: Do NOT modify this code. The content of this method is 496 * always regenerated by the Form Editor. 497 * 498 * @param initEntries Inital remote ADDE entries to edit. 499 */ 500 @SuppressWarnings("unchecked") 501 // <editor-fold defaultstate="collapsed" desc="Generated Code"> 502 private void initComponents(final List<RemoteAddeEntry> initEntries) { 503 assert SwingUtilities.isEventDispatchThread(); 504 entryPanel = new JPanel(); 505 serverLabel = new JLabel(); 506 serverField = new JTextField(); 507 datasetLabel = new JLabel(); 508 datasetField = new McVTextField(); 509 acctBox = new JCheckBox(); 510 userLabel = new JLabel(); 511 userField = new McVTextField(); 512 projLabel = new JLabel(); 513 projField = new JTextField(); 514 capBox = new JCheckBox(); 515 typePanel = new JPanel(); 516 imageBox = new JCheckBox(); 517 pointBox = new JCheckBox(); 518 gridBox = new JCheckBox(); 519 textBox = new JCheckBox(); 520 navBox = new JCheckBox(); 521 radarBox = new JCheckBox(); 522 statusPanel = new JPanel(); 523 statusLabel = new JLabel(); 524 verifyAddButton = new JButton(); 525 verifyServer = new JButton(); 526 addServer = new JButton(); 527 cancelButton = new JButton(); 528 529 boolean forceCaps = getForceMcxCaps(); 530 datasetField.setUppercase(forceCaps); 531 userField.setUppercase(forceCaps); 532 533 if (initEntries == RemoteAddeEntry.INVALID_ENTRIES) { 534 setTitle("Add Remote Dataset"); 535 } else { 536 setTitle("Edit Remote Dataset"); 537 } 538 setResizable(false); 539 setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 540 addWindowListener(new java.awt.event.WindowAdapter() { 541 public void windowClosed(WindowEvent evt) { 542 formWindowClosed(evt); 543 } 544 }); 545 546 serverLabel.setText("Server:"); 547 if (serverText != null) { 548 serverField.setText(serverText); 549 } 550 551 datasetLabel.setText("Dataset:"); 552 if (datasetText != null) { 553 datasetField.setText(datasetText); 554 } 555 556 acctBox.setText("Specify accounting information:"); 557 acctBox.addActionListener(this::acctBoxActionPerformed); 558 559 userLabel.setText("Username:"); 560 userField.setEnabled(acctBox.isSelected()); 561 562 projLabel.setText("Project #:"); 563 projField.setEnabled(acctBox.isSelected()); 564 565 capBox.setText("Automatically capitalize dataset and username?"); 566 capBox.setSelected(forceCaps); 567 capBox.addActionListener(this::capBoxActionPerformed); 568 569 DocumentListener inputListener = new DocumentListener() { 570 public void changedUpdate(DocumentEvent evt) { 571 reactToValueChanges(); 572 } 573 public void insertUpdate(DocumentEvent evt) { 574 if (inErrorState) { 575 verifyAddButton.setEnabled(true); 576 verifyServer.setEnabled(true); 577 inErrorState = false; 578 resetBadFields(); 579 } 580 } 581 public void removeUpdate(DocumentEvent evt) { 582 if (inErrorState) { 583 verifyAddButton.setEnabled(true); 584 verifyServer.setEnabled(true); 585 inErrorState = false; 586 resetBadFields(); 587 } 588 } 589 }; 590 591 serverField.getDocument().addDocumentListener(inputListener); 592 datasetField.getDocument().addDocumentListener(inputListener); 593 userField.getDocument().addDocumentListener(inputListener); 594 projField.getDocument().addDocumentListener(inputListener); 595 596 GroupLayout entryPanelLayout = new GroupLayout(entryPanel); 597 entryPanel.setLayout(entryPanelLayout); 598 entryPanelLayout.setHorizontalGroup( 599 entryPanelLayout.createParallelGroup(LEADING) 600 .addGroup(entryPanelLayout.createSequentialGroup() 601 .addGroup(entryPanelLayout.createParallelGroup(LEADING) 602 .addComponent(serverLabel, TRAILING) 603 .addComponent(datasetLabel, TRAILING) 604 .addComponent(userLabel, TRAILING) 605 .addComponent(projLabel, TRAILING)) 606 .addPreferredGap(RELATED) 607 .addGroup(entryPanelLayout.createParallelGroup(LEADING) 608 .addComponent(serverField, DEFAULT_SIZE, 419, Short.MAX_VALUE) 609 .addComponent(capBox) 610 .addComponent(acctBox) 611 .addComponent(datasetField, DEFAULT_SIZE, 419, Short.MAX_VALUE) 612 .addComponent(userField, DEFAULT_SIZE, 419, Short.MAX_VALUE) 613 .addComponent(projField, DEFAULT_SIZE, 419, Short.MAX_VALUE)) 614 .addContainerGap()) 615 ); 616 entryPanelLayout.setVerticalGroup( 617 entryPanelLayout.createParallelGroup(LEADING) 618 .addGroup(entryPanelLayout.createSequentialGroup() 619 .addGroup(entryPanelLayout.createParallelGroup(BASELINE) 620 .addComponent(serverLabel) 621 .addComponent(serverField, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE)) 622 .addPreferredGap(RELATED) 623 .addGroup(entryPanelLayout.createParallelGroup(BASELINE) 624 .addComponent(datasetLabel) 625 .addComponent(datasetField, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE)) 626 .addGap(16, 16, 16) 627 .addComponent(acctBox) 628 .addPreferredGap(RELATED) 629 .addGroup(entryPanelLayout.createParallelGroup(BASELINE) 630 .addComponent(userLabel) 631 .addComponent(userField, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE)) 632 .addPreferredGap(RELATED) 633 .addGroup(entryPanelLayout.createParallelGroup(BASELINE) 634 .addComponent(projLabel) 635 .addComponent(projField, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE)) 636 .addPreferredGap(RELATED) 637 .addComponent(capBox) 638 .addGap(0, 0, Short.MAX_VALUE)) 639 ); 640 641 typePanel.setBorder(BorderFactory.createTitledBorder("Dataset Types")); 642 643 ActionListener typeInputListener = evt -> { 644 if (inErrorState) { 645 verifyAddButton.setEnabled(true); 646 verifyServer.setEnabled(true); 647 inErrorState = false; 648 resetBadFields(); 649 } 650 }; 651 652 imageBox.setText("Image"); 653 imageBox.addActionListener(typeInputListener); 654 typePanel.add(imageBox); 655 656 pointBox.setText("Point"); 657 pointBox.addActionListener(typeInputListener); 658 typePanel.add(pointBox); 659 660 gridBox.setText("Grid"); 661 gridBox.addActionListener(typeInputListener); 662 typePanel.add(gridBox); 663 664 textBox.setText("Text"); 665 textBox.addActionListener(typeInputListener); 666 typePanel.add(textBox); 667 668 navBox.setText("Navigation"); 669 navBox.addActionListener(typeInputListener); 670 typePanel.add(navBox); 671 672 radarBox.setText("Radar"); 673 radarBox.addActionListener(typeInputListener); 674 typePanel.add(radarBox); 675 676 statusPanel.setBorder(BorderFactory.createTitledBorder("Status")); 677 678 statusLabel.setText("Please provide the address of a remote ADDE server."); 679 680 GroupLayout statusPanelLayout = new GroupLayout(statusPanel); 681 statusPanel.setLayout(statusPanelLayout); 682 statusPanelLayout.setHorizontalGroup( 683 statusPanelLayout.createParallelGroup(LEADING) 684 .addGroup(statusPanelLayout.createSequentialGroup() 685 .addContainerGap() 686 .addComponent(statusLabel) 687 .addContainerGap(154, Short.MAX_VALUE)) 688 ); 689 statusPanelLayout.setVerticalGroup( 690 statusPanelLayout.createParallelGroup(LEADING) 691 .addGroup(statusPanelLayout.createSequentialGroup() 692 .addComponent(statusLabel) 693 .addContainerGap(DEFAULT_SIZE, Short.MAX_VALUE)) 694 ); 695 696 if (initEntries.equals(RemoteAddeEntry.INVALID_ENTRIES)) { 697 verifyAddButton.setText("Verify and Add Server"); 698 } else { 699 verifyAddButton.setText("Verify and Save Changes"); 700 } 701 verifyAddButton.addActionListener(evt -> { 702 if (initEntries == RemoteAddeEntry.INVALID_ENTRIES) 703 verifyAddButtonActionPerformed(evt); 704 else 705 verifyEditButtonActionPerformed(evt); 706 }); 707 708 if (initEntries == RemoteAddeEntry.INVALID_ENTRIES) { 709 verifyServer.setText("Verify Server"); 710 } else { 711 verifyServer.setText("Verify Changes"); 712 } 713 verifyServer.addActionListener(evt -> verifyServerActionPerformed(evt)); 714 715 if (initEntries.equals(RemoteAddeEntry.INVALID_ENTRIES)) { 716 addServer.setText("Add Server"); 717 } else { 718 addServer.setText("Save Changes"); 719 } 720 addServer.addActionListener(evt -> { 721 if (initEntries == RemoteAddeEntry.INVALID_ENTRIES) { 722 addServerActionPerformed(evt); 723 } else { 724 editServerActionPerformed(evt); 725 } 726 }); 727 728 cancelButton.setText("Cancel"); 729 cancelButton.addActionListener(this::cancelButtonActionPerformed); 730 731 GroupLayout layout = new GroupLayout(getContentPane()); 732 getContentPane().setLayout(layout); 733 layout.setHorizontalGroup( 734 layout.createParallelGroup(LEADING) 735 .addGroup(layout.createSequentialGroup() 736 .addContainerGap() 737 .addGroup(layout.createParallelGroup(LEADING) 738 .addComponent(statusPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE) 739 .addComponent(typePanel, 0, 0, Short.MAX_VALUE) 740 .addComponent(entryPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE) 741 .addGroup(layout.createSequentialGroup() 742 .addComponent(verifyAddButton) 743 .addPreferredGap(RELATED) 744 .addComponent(verifyServer) 745 .addPreferredGap(RELATED) 746 .addComponent(addServer) 747 .addPreferredGap(RELATED) 748 .addComponent(cancelButton))) 749 .addContainerGap()) 750 ); 751 layout.setVerticalGroup( 752 layout.createParallelGroup(LEADING) 753 .addGroup(layout.createSequentialGroup() 754 .addContainerGap() 755 .addComponent(entryPanel, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE) 756 .addPreferredGap(UNRELATED) 757 .addComponent(typePanel, PREFERRED_SIZE, 57, PREFERRED_SIZE) 758 .addGap(18, 18, 18) 759 .addComponent(statusPanel, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE) 760 .addGap(18, 18, 18) 761 .addGroup(layout.createParallelGroup(BASELINE) 762 .addComponent(verifyServer) 763 .addComponent(addServer) 764 .addComponent(cancelButton) 765 .addComponent(verifyAddButton)) 766 .addContainerGap(17, Short.MAX_VALUE)) 767 ); 768 769 if ((initEntries != null) && !RemoteAddeEntry.INVALID_ENTRIES.equals(initEntries)) { 770 RemoteAddeEntry initEntry = initEntries.get(0); 771 boolean hasSystemEntry = false; 772 for (RemoteAddeEntry entry : initEntries) { 773 if (entry.getEntrySource() == EntrySource.SYSTEM) { 774 initEntry = entry; 775 hasSystemEntry = true; 776 break; 777 } 778 } 779 serverField.setText(initEntry.getAddress()); 780 datasetField.setText(initEntry.getGroup()); 781 782 if (!RemoteAddeEntry.DEFAULT_ACCOUNT.equals(initEntry.getAccount())) { 783 acctBox.setSelected(true); 784 userField.setEnabled(true); 785 userField.setText(initEntry.getAccount().getUsername()); 786 projField.setEnabled(true); 787 projField.setText(initEntry.getAccount().getProject()); 788 } 789 790 if (hasSystemEntry) { 791 serverField.setEnabled(false); 792 datasetField.setEnabled(false); 793 acctBox.setEnabled(false); 794 userField.setEnabled(false); 795 projField.setEnabled(false); 796 capBox.setEnabled(false); 797 } 798 799 for (RemoteAddeEntry entry : initEntries) { 800 boolean nonDefaultSource = entry.getEntrySource() != EntrySource.SYSTEM; 801 if (entry.getEntryType() == EntryType.IMAGE) { 802 imageBox.setSelected(true); 803 imageBox.setEnabled(nonDefaultSource); 804 } else if (entry.getEntryType() == EntryType.POINT) { 805 pointBox.setSelected(true); 806 pointBox.setEnabled(nonDefaultSource); 807 } else if (entry.getEntryType() == EntryType.GRID) { 808 gridBox.setSelected(true); 809 gridBox.setEnabled(nonDefaultSource); 810 } else if (entry.getEntryType() == EntryType.TEXT) { 811 textBox.setSelected(true); 812 textBox.setEnabled(nonDefaultSource); 813 } else if (entry.getEntryType() == EntryType.NAV) { 814 navBox.setSelected(true); 815 navBox.setEnabled(nonDefaultSource); 816 } else if (entry.getEntryType() == EntryType.RADAR) { 817 radarBox.setSelected(true); 818 radarBox.setEnabled(nonDefaultSource); 819 } 820 } 821 } 822 pack(); 823 }// </editor-fold> 824 825 private void acctBoxActionPerformed(ActionEvent evt) { 826 assert SwingUtilities.isEventDispatchThread(); 827 resetBadFields(); 828 boolean enabled = acctBox.isSelected(); 829 userField.setEnabled(enabled); 830 projField.setEnabled(enabled); 831 verifyAddButton.setEnabled(true); 832 verifyServer.setEnabled(true); 833 } 834 835 private void capBoxActionPerformed(ActionEvent evt) { 836 assert SwingUtilities.isEventDispatchThread(); 837 boolean forceCaps = capBox.isSelected(); 838 datasetField.setUppercase(forceCaps); 839 userField.setUppercase(forceCaps); 840 setForceMcxCaps(forceCaps); 841 if (!forceCaps) { 842 return; 843 } 844 datasetField.setText(safeGetText(datasetField).toUpperCase()); 845 userField.setText(safeGetText(userField).toUpperCase()); 846 } 847 848 private void verifyAddButtonActionPerformed(ActionEvent evt) { 849 verifyInput(EditorAction.VERIFYING_AND_ADDING); 850 } 851 852 private void handleVerifyAdd() { 853 if (!anyBadFields()) { 854 setEditorAction(EditorAction.ADDED_VERIFIED); 855 addEntry(); 856 } else { 857 inErrorState = true; 858 verifyAddButton.setEnabled(false); 859 verifyServer.setEnabled(false); 860 } 861 } 862 863 private void verifyEditButtonActionPerformed(ActionEvent evt) { 864 verifyInput(EditorAction.VERIFYING_AND_EDITING); 865 } 866 867 private void handleVerifyEdit() { 868 if (!anyBadFields()) { 869 setEditorAction(EditorAction.EDITED_VERIFIED); 870 editEntry(); 871 } else { 872 inErrorState = true; 873 verifyAddButton.setEnabled(false); 874 verifyServer.setEnabled(false); 875 } 876 } 877 878 private void cancelButtonActionPerformed(ActionEvent evt) { 879 setEditorAction(EditorAction.CANCELLED); 880 disposeDisplayable(false); 881 Thread t = new Thread() { 882 @Override public void run() { 883 if (exec != null) { 884 exec.shutdownNow(); 885 } 886 } 887 }; 888 t.start(); 889 } 890 891 private void formWindowClosed(WindowEvent evt) { 892 setEditorAction(EditorAction.CANCELLED); 893 disposeDisplayable(false); 894 } 895 896 private void verifyServerActionPerformed(ActionEvent evt) { 897 verifyInput(EditorAction.VERIFYING); 898 if (anyBadFields()) { 899 // save poll widget state 900 // toggle a "listen for *any* input event" switch to on 901// invalidEntries.clear(); 902// invalidEntries.addAll(pollWidgets(false)); 903 inErrorState = true; 904 verifyAddButton.setEnabled(false); 905 verifyServer.setEnabled(false); 906 } 907 } 908 909 private void addServerActionPerformed(ActionEvent evt) { 910 setEditorAction(EditorAction.ADDED); 911 addEntry(); 912 } 913 914 private void editServerActionPerformed(ActionEvent evt) { 915 setEditorAction(EditorAction.EDITED); 916 editEntry(); 917 } 918 919 private void reactToValueChanges() { 920 assert SwingUtilities.isEventDispatchThread(); 921 if (inErrorState) { 922 verifyAddButton.setEnabled(true); 923 verifyServer.setEnabled(true); 924 inErrorState = false; 925 resetBadFields(); 926 } 927 } 928 929 /** 930 * Attempt to verify a {@link Set} of {@link RemoteAddeEntry}s. Useful for 931 * checking a {@literal "MCTABLE.TXT"} after importing. 932 * 933 * @param entries {@code Set} of remote ADDE entries to validate. Cannot 934 * be {@code null}. 935 * 936 * @return {@code Set} of {@code RemoteAddeEntry}s that McIDAS-V was able 937 * to connect to. 938 * 939 * @throws NullPointerException if {@code entries} is {@code null}. 940 */ 941 public Set<RemoteAddeEntry> checkHosts(final Set<RemoteAddeEntry> entries) { 942 requireNonNull(entries, "entries cannot be null"); 943 Set<RemoteAddeEntry> goodEntries = newLinkedHashSet(); 944 Set<String> checkedHosts = newLinkedHashSet(); 945 Map<String, Boolean> hostStatus = newMap(); 946 for (RemoteAddeEntry entry : entries) { 947 String host = entry.getAddress(); 948 if (hostStatus.get(host).equals(Boolean.TRUE)) { 949 goodEntries.add(entry); 950 } else { 951 checkedHosts.add(host); 952 if (RemoteAddeEntry.checkHost(entry)) { 953 goodEntries.add(entry); 954 hostStatus.put(host, Boolean.TRUE); 955 } else { 956 hostStatus.put(host, Boolean.FALSE); 957 } 958 } 959 } 960 return goodEntries; 961 } 962 963 private Thread makeCheckThread(final EditorAction action, 964 final Set<RemoteAddeEntry> entries) 965 { 966 return new Thread() { 967 @Override public void run() { 968 logger.trace("checking entries..."); 969 checkGroups(action, entries); 970 } 971 }; 972 } 973 974 private void setCheckBoxes(final Set<RemoteAddeEntry> verified) { 975 SwingUtilities.invokeLater(() -> { 976 EnumSet<EntryType> presentTypes = 977 EnumSet.noneOf(EntryType.class); 978 979 presentTypes.addAll( 980 verified.stream() 981 .map(RemoteAddeEntry::getEntryType) 982 .collect(Collectors.toList())); 983 984 imageBox.setSelected(presentTypes.contains(EntryType.IMAGE)); 985 pointBox.setSelected(presentTypes.contains(EntryType.POINT)); 986 gridBox.setSelected(presentTypes.contains(EntryType.GRID)); 987 textBox.setSelected(presentTypes.contains(EntryType.TEXT)); 988 navBox.setSelected(presentTypes.contains(EntryType.NAV)); 989 radarBox.setSelected(presentTypes.contains(EntryType.RADAR)); 990 }); 991 } 992 993 public Set<RemoteAddeEntry> checkGroups(final EditorAction action, 994 final Set<RemoteAddeEntry> entries) 995 { 996 requireNonNull(entries, "entries cannot be null"); 997 if (entries.isEmpty()) { 998 return Collections.emptySet(); 999 } 1000 1001 exec = Executors.newFixedThreadPool(POOL); 1002 1003 Set<RemoteAddeEntry> verified = newLinkedHashSet(entries.size()); 1004 Collection<AddeStatus> statuses = EnumSet.noneOf(AddeStatus.class); 1005 1006 CompletionService<StatusWrapper> ecs = 1007 new ExecutorCompletionService<>(exec); 1008 1009 Map<RemoteAddeEntry, AddeStatus> entry2Status = 1010 new LinkedHashMap<>(entries.size()); 1011 1012 // submit new verification tasks to the pool's queue 1013 // ... 1014 // (apologies for the pun?) 1015 for (RemoteAddeEntry entry : entries) { 1016 StatusWrapper pairing = new StatusWrapper(entry); 1017 ecs.submit(new VerifyEntryTask(pairing)); 1018 } 1019 1020 // use completion service magic to only deal with finished 1021 // verification tasks 1022 try { 1023 int checkedEntries = 0; 1024 while (checkedEntries != entries.size()) { 1025 1026 // determine if the user has cancelled their verification 1027 // request 1028 if ((exec != null) && exec.isShutdown()) { 1029 break; 1030 } 1031 1032 Future<StatusWrapper> future = 1033 ecs.poll(300, TimeUnit.MILLISECONDS); 1034 1035 if (future != null && future.isDone()) { 1036 StatusWrapper pairing = future.get(); 1037 RemoteAddeEntry entry = pairing.getEntry(); 1038 AddeStatus status = pairing.getStatus(); 1039 setStatus(entry.getEntryText()+": attempting verification..."); 1040 statuses.add(status); 1041 entry2Status.put(entry, status); 1042 if (status == AddeStatus.OK) { 1043 verified.add(entry); 1044 setStatus("Found accessible "+entry.getEntryType().toString().toLowerCase()+" data."); 1045 } 1046 checkedEntries++; 1047 setCheckBoxes(verified); 1048 } 1049 } 1050 } catch (InterruptedException e) { 1051 LogUtil.logException("interrupted while checking ADDE entries", e); 1052 } catch (ExecutionException e) { 1053 LogUtil.logException("ADDE validation execution error", e); 1054 } finally { 1055 exec.shutdown(); 1056 } 1057 1058 if (!statuses.contains(AddeStatus.OK)) { 1059 if (statuses.contains(AddeStatus.BAD_ACCOUNTING)) { 1060 setStatus("Incorrect accounting information."); 1061 setBadField(userField, true); 1062 setBadField(projField, true); 1063 } else if (statuses.contains(AddeStatus.BAD_GROUP)) { 1064 setStatus("Dataset does not appear to be valid."); 1065 setBadField(datasetField, true); 1066 } else if (statuses.contains(AddeStatus.BAD_SERVER)) { 1067 setStatus("Could not connect to the ADDE server."); 1068 setBadField(serverField, true); 1069 } else { 1070 logger.debug("no statuses are available; user may have cancelled"); 1071 } 1072 } else { 1073 setStatus("Finished verifying."); 1074 } 1075 1076 if (EditorAction.VERIFYING_AND_ADDING.equals(action)) { 1077 handleVerifyAdd(); 1078 } else if (EditorAction.VERIFYING_AND_EDITING.equals(action)) { 1079 handleVerifyEdit(); 1080 } 1081 return verified; 1082 } 1083 1084 private static Map<RemoteAddeEntry, AddeStatus> bulkPut(final Collection<RemoteAddeEntry> entries, final AddeStatus status) { 1085 Map<RemoteAddeEntry, AddeStatus> map = new LinkedHashMap<>(entries.size()); 1086 for (RemoteAddeEntry entry : entries) { 1087 map.put(entry, status); 1088 } 1089 return map; 1090 } 1091 1092 /** 1093 * Associates a {@link RemoteAddeEntry} with one of the states from 1094 * {@link AddeStatus}. 1095 */ 1096 private static class StatusWrapper { 1097 /** */ 1098 private final RemoteAddeEntry entry; 1099 1100 /** Current {@literal "status"} of {@link #entry}. */ 1101 private AddeStatus status; 1102 1103 /** 1104 * Builds an entry/status pairing. 1105 * 1106 * @param entry The {@code RemoteAddeEntry} to wrap up. 1107 * 1108 * @throws NullPointerException if {@code entry} is {@code null}. 1109 */ 1110 public StatusWrapper(final RemoteAddeEntry entry) { 1111 requireNonNull(entry, "cannot create a entry/status pair with a null descriptor"); 1112 this.entry = entry; 1113 } 1114 1115 /** 1116 * Set the {@literal "status"} of this {@link #entry} to a given 1117 * {@link AddeStatus}. 1118 * 1119 * @param status New status of {@code entry}. 1120 */ 1121 public void setStatus(AddeStatus status) { 1122 this.status = status; 1123 } 1124 1125 /** 1126 * Returns the current {@literal "status"} of {@link #entry}. 1127 * 1128 * @return One of {@link AddeStatus}. 1129 */ 1130 public AddeStatus getStatus() { 1131 return status; 1132 } 1133 1134 /** 1135 * Returns the {@link RemoteAddeEntry} stored in this wrapper. 1136 * 1137 * @return {@link #entry} 1138 */ 1139 public RemoteAddeEntry getEntry() { 1140 return entry; 1141 } 1142 } 1143 1144 /** 1145 * Represents an ADDE entry verification task. These are executed asynchronously 1146 * by the completion service within {@link RemoteEntryEditor#checkGroups(Set)}. 1147 */ 1148 private class VerifyEntryTask implements Callable<StatusWrapper> { 1149 private final StatusWrapper entryStatus; 1150 public VerifyEntryTask(final StatusWrapper descStatus) { 1151 requireNonNull(descStatus, "cannot verify or set status of a null descriptor/status pair"); 1152 this.entryStatus = descStatus; 1153 } 1154 1155 @Override public StatusWrapper call() throws Exception { 1156 entryStatus.setStatus(RemoteAddeEntry.checkEntry(entryStatus.getEntry())); 1157 return entryStatus; 1158 } 1159 } 1160 1161 private class VerifyHostTask implements Callable<StatusWrapper> { 1162 private final StatusWrapper entryStatus; 1163 public VerifyHostTask(final StatusWrapper descStatus) { 1164 entryStatus = requireNonNull(descStatus, "cannot verify or set status of a null descriptor/status pair"); 1165 } 1166 @Override public StatusWrapper call() throws Exception { 1167 boolean validHost = RemoteAddeEntry.checkHost(entryStatus.getEntry()); 1168 if (validHost) { 1169 entryStatus.setStatus(AddeStatus.OK); 1170 } else { 1171 entryStatus.setStatus(AddeStatus.BAD_SERVER); 1172 } 1173 return entryStatus; 1174 } 1175 } 1176 1177 // Variables declaration - do not modify 1178 private JCheckBox acctBox; 1179 private JButton addServer; 1180 private JButton cancelButton; 1181 private JCheckBox capBox; 1182 private McVTextField datasetField; 1183 private JLabel datasetLabel; 1184 private JPanel entryPanel; 1185 private JCheckBox gridBox; 1186 private JCheckBox imageBox; 1187 private JCheckBox navBox; 1188 private JCheckBox pointBox; 1189 private JTextField projField; 1190 private JLabel projLabel; 1191 private JCheckBox radarBox; 1192 private JTextField serverField; 1193 private JLabel serverLabel; 1194 private JLabel statusLabel; 1195 private JPanel statusPanel; 1196 private JCheckBox textBox; 1197 private JPanel typePanel; 1198 private McVTextField userField; 1199 private JLabel userLabel; 1200 private JButton verifyAddButton; 1201 private JButton verifyServer; 1202 // End of variables declaration 1203}