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