001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2024 005 * Space Science and Engineering Center (SSEC) 006 * University of Wisconsin - Madison 007 * 1225 W. Dayton Street, Madison, WI 53706, USA 008 * https://www.ssec.wisc.edu/mcidas/ 009 * 010 * All Rights Reserved 011 * 012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and 013 * some McIDAS-V source code is based on IDV and VisAD source code. 014 * 015 * McIDAS-V is free software; you can redistribute it and/or modify 016 * it under the terms of the GNU Lesser Public License as published by 017 * the Free Software Foundation; either version 3 of the License, or 018 * (at your option) any later version. 019 * 020 * McIDAS-V is distributed in the hope that it will be useful, 021 * but WITHOUT ANY WARRANTY; without even the implied warranty of 022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 023 * GNU Lesser Public License for more details. 024 * 025 * You should have received a copy of the GNU Lesser Public License 026 * along with this program. If not, see https://www.gnu.org/licenses/. 027 */ 028package edu.wisc.ssec.mcidasv.supportform; 029 030import static java.util.Objects.requireNonNull; 031 032import java.awt.EventQueue; 033 034import javax.swing.JFrame; 035import javax.swing.JPanel; 036import javax.swing.text.JTextComponent; 037 038import net.miginfocom.swing.MigLayout; 039 040import javax.swing.JFileChooser; 041import javax.swing.JLabel; 042import javax.swing.JOptionPane; 043import javax.swing.JScrollPane; 044import javax.swing.JTextField; 045import javax.swing.JTextArea; 046import javax.swing.JButton; 047import javax.swing.JCheckBox; 048import java.awt.event.MouseAdapter; 049import java.awt.event.MouseEvent; 050import java.awt.event.ActionListener; 051import java.awt.event.ActionEvent; 052 053import org.slf4j.Logger; 054import org.slf4j.LoggerFactory; 055import ucar.unidata.idv.IdvObjectStore; 056import ucar.unidata.idv.IntegratedDataViewer; 057import ucar.unidata.idv.ui.IdvUIManager; 058 059import edu.wisc.ssec.mcidasv.util.BackgroundTask; 060import edu.wisc.ssec.mcidasv.util.CollectionHelpers; 061import edu.wisc.ssec.mcidasv.util.FocusTraveller; 062 063import java.io.File; 064import java.nio.file.Files; 065import java.nio.file.Paths; 066import java.util.List; 067import java.util.concurrent.ExecutorService; 068import java.util.concurrent.Executors; 069 070/** 071 * This class handles all the GUI elements of a McIDAS-V support request. 072 */ 073public class SupportForm extends JFrame { 074 075 private static final Logger logger = 076 LoggerFactory.getLogger(SupportForm.class); 077 078 public static final String PROP_SUPPORTREQ_BUNDLE = "mcv.supportreq.bundle"; 079 080 public static final String PROP_SUPPORTREQ_CC = "mcv.supportreq.cc"; 081 082 public static final String PROP_SUPPORTREQ_CONFIRMEMAIL = "mcv.supportreq.confirmedemail"; 083 084 private static final String HELP_ID = "idv.tools.supportrequestform"; 085 086 private static ExecutorService exec = Executors.newCachedThreadPool(); 087 088 private final IdvObjectStore store; 089 090 private final StateCollector collector; 091 092 private final CancelListener listener = new CancelListener(); 093 094 private JPanel contentPane; 095 private JTextField userField; 096 private JTextField emailField; 097 private JTextField confirmField; 098 private JTextField organizationField; 099 private JTextField subjectField; 100 private JTextField attachmentOneField; 101 private JTextField attachmentTwoField; 102 private JTextArea descriptionArea; 103 private JCheckBox bundleCheckBox; 104 private JCheckBox ccCheckBox; 105 private JButton sendButton; 106 private JButton cancelButton; 107 private JButton helpButton; 108 109 /** 110 * Creates a support request form that collects information about 111 * the current McIDAS-V session. 112 * 113 * @param store Storage for persisted user input. Should not be {@code null}. 114 * @param collector Collects information about the current session. 115 */ 116 public SupportForm(IdvObjectStore store, StateCollector collector) { 117 this.store = requireNonNull(store); 118 this.collector = requireNonNull(collector); 119 initComponents(); 120 unpersistInput(); 121 otherDoFocusThingNow(); 122 } 123 124 /** 125 * Saves user input for the following: name, email address, email address 126 * confirmation, organization, whether or not to CC the user a copy, and 127 * whether or not a {@literal "state"} bundle should be included. 128 * 129 * <p>You should initialize the GUI components before calling this method. 130 */ 131 private void persistInput() { 132 store.put(IdvUIManager.PROP_HELP_NAME, getUser()); 133 store.put(IdvUIManager.PROP_HELP_EMAIL, getEmail()); 134 store.put(PROP_SUPPORTREQ_CONFIRMEMAIL, getConfirmedEmail()); 135 store.put(IdvUIManager.PROP_HELP_ORG, getOrganization()); 136 store.put(PROP_SUPPORTREQ_CC, getSendCopy()); 137 store.put(PROP_SUPPORTREQ_BUNDLE, getSendBundle()); 138 store.save(); 139 } 140 141 /** 142 * Loads user input for the following: name, email address, email address 143 * confirmation, organization, whether or not to CC the user a copy, and 144 * whether or not a {@literal "state"} bundle should be included. 145 * 146 * <p>You should initialize the GUI components before calling this method. 147 */ 148 private void unpersistInput() { 149 userField.setText(store.get(IdvUIManager.PROP_HELP_NAME, "")); 150 emailField.setText(store.get(IdvUIManager.PROP_HELP_EMAIL, "")); 151 confirmField.setText(store.get(PROP_SUPPORTREQ_CONFIRMEMAIL, "")); 152 organizationField.setText(store.get(IdvUIManager.PROP_HELP_ORG, "")); 153 ccCheckBox.setSelected(store.get(PROP_SUPPORTREQ_CC, true)); 154 bundleCheckBox.setSelected(store.get(PROP_SUPPORTREQ_BUNDLE, false)); 155 } 156 157 /** 158 * Create the frame. 159 */ 160 public void initComponents() { 161 setTitle("Request McIDAS-V Support"); 162 setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); 163 setBounds(100, 100, 682, 538); 164 contentPane = new JPanel(); 165 setContentPane(contentPane); 166 contentPane.setLayout(new MigLayout("", "[][grow]", "[][][][][][][grow][][][][][][]")); 167 168 JLabel nameLabel = new JLabel("Your Name:"); 169 contentPane.add(nameLabel, "cell 0 0,alignx right"); 170 171 userField = new JTextField(); 172 contentPane.add(userField, "cell 1 0,growx"); 173 userField.setName("user"); 174 userField.setColumns(10); 175 176 JLabel emailLabel = new JLabel("Your Email:"); 177 contentPane.add(emailLabel, "cell 0 1,alignx right"); 178 179 emailField = new JTextField(); 180 contentPane.add(emailField, "cell 1 1,growx"); 181 emailField.setName("email"); 182 emailField.setColumns(10); 183 184 JLabel confirmLabel = new JLabel("Confirm Email:"); 185 contentPane.add(confirmLabel, "cell 0 2,alignx right"); 186 187 confirmField = new JTextField(); 188 contentPane.add(confirmField, "cell 1 2,growx"); 189 confirmField.setName("confirm"); 190 confirmField.setColumns(10); 191 192 JLabel organizationLabel = new JLabel("Organization:"); 193 contentPane.add(organizationLabel, "cell 0 3,alignx right"); 194 195 organizationField = new JTextField(); 196 contentPane.add(organizationField, "cell 1 3,growx"); 197 organizationField.setName("organization"); 198 organizationField.setColumns(10); 199 200 JLabel subjectLabel = new JLabel("Subject:"); 201 contentPane.add(subjectLabel, "cell 0 4,alignx right"); 202 203 subjectField = new JTextField(); 204 contentPane.add(subjectField, "cell 1 4,growx"); 205 subjectField.setName("subject"); 206 subjectField.setColumns(10); 207 208 JLabel descriptiveLabel = new JLabel("Please provide a thorough description of the problem you encountered."); 209 contentPane.add(descriptiveLabel, "cell 1 5"); 210 211 JLabel descriptionLabel = new JLabel("Description:"); 212 contentPane.add(descriptionLabel, "cell 0 6,alignx right,aligny top"); 213 214 descriptionArea = new JTextArea(); 215 JScrollPane scrollPane = new JScrollPane(descriptionArea); 216 contentPane.add(scrollPane, "cell 1 6,grow"); 217 descriptionArea.setName("description"); 218 descriptionArea.setLineWrap(true); 219 descriptionArea.setWrapStyleWord(true); 220 descriptionArea.setColumns(20); 221 descriptionArea.setRows(6); 222 223 JLabel attachmentOneLabel = new JLabel("Attachment 1:"); 224 contentPane.add(attachmentOneLabel, "cell 0 7,alignx right"); 225 226 attachmentOneField = new JTextField(); 227 attachmentOneField.addMouseListener(new MouseAdapter() { 228 @Override public void mouseClicked(MouseEvent evt) { 229 attachmentOneFieldMouseClicked(evt); 230 } 231 }); 232 contentPane.add(attachmentOneField, "flowx,cell 1 7,growx"); 233 attachmentOneField.setName("attachment1"); 234 attachmentOneField.setColumns(10); 235 236 JButton attachmentOneButton = new JButton("Browse..."); 237 attachmentOneButton.addActionListener(new ActionListener() { 238 public void actionPerformed(ActionEvent evt) { 239 attachmentOneButtonActionPerformed(evt); 240 } 241 }); 242 contentPane.add(attachmentOneButton, "cell 1 7,alignx left"); 243 244 JLabel attachmentTwoLabel = new JLabel("Attachment 2:"); 245 contentPane.add(attachmentTwoLabel, "cell 0 8,alignx right"); 246 247 attachmentTwoField = new JTextField(); 248 attachmentTwoField.addMouseListener(new MouseAdapter() { 249 @Override public void mouseClicked(MouseEvent evt) { 250 attachmentTwoFieldMouseClicked(evt); 251 } 252 }); 253 contentPane.add(attachmentTwoField, "flowx,cell 1 8,growx"); 254 attachmentTwoField.setName("attachment2"); 255 attachmentTwoField.setColumns(10); 256 257 JButton attachmentTwoButton = new JButton("Browse..."); 258 attachmentTwoButton.addActionListener(new ActionListener() { 259 public void actionPerformed(ActionEvent evt) { 260 attachmentTwoButtonActionPerformed(evt); 261 } 262 }); 263 contentPane.add(attachmentTwoButton, "cell 1 8,alignx left"); 264 265 bundleCheckBox = new JCheckBox("Include current application state as a bundle."); 266 bundleCheckBox.setName("sendstate"); 267 contentPane.add(bundleCheckBox, "cell 1 9,alignx left"); 268 269 ccCheckBox = new JCheckBox("Send copy of support request to the email address I provided."); 270 ccCheckBox.setName("ccrequest"); 271 ccCheckBox.setSelected(true); 272 contentPane.add(ccCheckBox, "cell 1 10,alignx left"); 273 274 helpButton = new JButton("Help"); 275 helpButton.addActionListener(new ActionListener() { 276 public void actionPerformed(ActionEvent evt) { 277 ucar.unidata.ui.Help.getDefaultHelp().gotoTarget(HELP_ID); 278 } 279 }); 280 contentPane.add(helpButton, "flowx,cell 1 12,alignx right"); 281 282 cancelButton = new JButton("Cancel"); 283 cancelButton.addActionListener(listener); 284 contentPane.add(cancelButton, "cell 1 12,alignx right"); 285 286 sendButton = new JButton("Send Request"); 287 sendButton.addActionListener(new ActionListener() { 288 public void actionPerformed(ActionEvent evt) { 289 sendRequest(evt); 290 } 291 }); 292 contentPane.add(sendButton, "cell 1 12,alignx right"); 293 contentPane.setFocusTraversalPolicy(new FocusTraveller(userField, emailField, confirmField, organizationField, subjectField, descriptionArea, attachmentOneButton, attachmentTwoButton, bundleCheckBox, ccCheckBox, helpButton, cancelButton, sendButton)); 294 } 295 296 /** 297 * Checks {@link #emailField} and {@link #confirmField} to see if they 298 * match (case is ignored). 299 * 300 * @return {@code true} if there is a match, {@code false} otherwise. 301 */ 302 public boolean checkEmailAddresses() { 303 return emailField.getText().equalsIgnoreCase(confirmField.getText()); 304 } 305 306 /** 307 * Returns whatever occupies {@link #userField}. 308 * 309 * @return User's name. 310 */ 311 public String getUser() { 312 return userField.getText(); 313 } 314 315 /** 316 * Returns whatever currently lives in {@link #emailField}. 317 * 318 * @return User's email address. 319 */ 320 public String getEmail() { 321 return emailField.getText(); 322 } 323 324 /** 325 * Returns whatever currently lives in {@link #confirmField}. 326 * 327 * @return User's confirmed email address. 328 */ 329 public String getConfirmedEmail() { 330 return confirmField.getText(); 331 } 332 333 /** 334 * Returns whatever resides in {@link #subjectField}. 335 * 336 * @return Subject of the support request. 337 */ 338 public String getSubject() { 339 return subjectField.getText(); 340 } 341 342 /** 343 * Returns whatever has commandeered {@link #organizationField}. 344 * 345 * @return Organization to which the user belongs. 346 */ 347 public String getOrganization() { 348 return organizationField.getText(); 349 } 350 351 /** 352 * Returns whatever is ensconced inside {@link #descriptionArea}. 353 * 354 * @return Body of the user's email. 355 */ 356 public String getDescription() { 357 return descriptionArea.getText(); 358 } 359 360 /** 361 * Checks whether or not the user has attached a file in the 362 * {@literal "first file"} slot. 363 * 364 * @return {@code true} if there's a file, {@code false} otherwise. 365 */ 366 public boolean hasAttachmentOne() { 367 return new File(attachmentOneField.getText()).exists(); 368 } 369 370 /** 371 * Checks whether or not the user has attached a file in the 372 * {@literal "second file"} slot. 373 * 374 * @return {@code true} if there's a file, {@code false} otherwise. 375 */ 376 public boolean hasAttachmentTwo() { 377 return new File(attachmentTwoField.getText()).exists(); 378 } 379 380 /** 381 * Returns whatever file path has monopolized {@link #attachmentOneField}. 382 * 383 * @return Path to the first file attachment, or a blank string if no file 384 * has been selected. 385 */ 386 public String getAttachmentOne() { 387 return attachmentOneField.getText(); 388 } 389 390 /** 391 * Returns whatever file path has appeared within 392 * {@link #attachmentTwoField}. 393 * 394 * @return Path to the second file attachment, or a blank string if no 395 * file has been selected. 396 */ 397 public String getAttachmentTwo() { 398 return attachmentTwoField.getText(); 399 } 400 401 // TODO: javadocs! 402 public boolean getSendCopy() { 403 return ccCheckBox.isSelected(); 404 } 405 406 public boolean getSendBundle() { 407 return bundleCheckBox.isSelected(); 408 } 409 410 public byte[] getExtraState() { 411 return collector.getContents(); 412 } 413 414 public String getExtraStateName() { 415 return collector.getExtraAttachmentName(); 416 } 417 418 public boolean canBundleState() { 419 return collector.canBundleState(); 420 } 421 422 public byte[] getBundledState() { 423 return collector.getBundledState(); 424 } 425 426 public String getBundledStateName() { 427 return collector.getBundleAttachmentName(); 428 } 429 430 /** 431 * Determine if {@code mcidasv.log} can be sent to the Help Desk. 432 * 433 * @return {@code true} if {@code mcidasv.log} exists, {@code false} 434 * otherwise. 435 */ 436 public boolean canSendLog() { 437 String path = collector.getLogPath(); 438 if (path == null || path.isEmpty()) { 439 return false; 440 } 441 return new File(path).exists(); 442 } 443 444 /** 445 * Get path to where {@code mcidasv.log} <b>should</b> be located. 446 * 447 * @return String representing the full path to the user's 448 * {@code mcidasv.log}. Note: <b>the file may not exist!</b> 449 */ 450 public String getLogPath() { 451 return collector.getLogPath(); 452 } 453 454 /** 455 * Determine if {@code RESOLV.SRV} can be sent to the Help Desk. 456 * 457 * @return {@code true} if {@code RESOLV.SRV} exists, {@code false} 458 * otherwise. 459 */ 460 public boolean canSendResolvSrv() { 461 String path = collector.getResolvSrvPath(); 462 boolean result = false; 463 if ((path != null) && !path.isEmpty()) { 464 result = Files.exists(Paths.get(path)); 465 } 466 return result; 467 } 468 469 /** 470 * Get the path to where the user's {@code RESOLV.SRV} file <b>should</b> 471 * be located. 472 * 473 * @return Path to {@code RESOLV.SRV}. Note: <b>the file may not exist!</b> 474 */ 475 public String getResolvSrvPath() { 476 return collector.getResolvSrvPath(); 477 } 478 479 /** 480 * Get path to McV prefs file. 481 * 482 * @return 483 */ 484 public String getPrefsPath() { 485 return collector.getPrefsPath(); 486 } 487 488 // TODO: dialogs are bad news bears. 489 public void showSuccess() { 490 setVisible(false); 491 dispose(); 492 JOptionPane.showMessageDialog(null, "Support request sent successfully.", "Success", JOptionPane.DEFAULT_OPTION); 493 } 494 495 // TODO: dialogs are bad news hares. 496 public void showFailure(final String reason) { 497 String msg = ""; 498 if (reason == null || reason.isEmpty()) { 499 msg = "Error sending request, could not determine cause."; 500 } else { 501 msg = "Error sending request:\n"+reason; 502 } 503 JOptionPane.showMessageDialog(this, msg, "Problem sending support request", JOptionPane.ERROR_MESSAGE); 504 if (sendButton != null) { 505 sendButton.setEnabled(true); 506 } 507 } 508 509 /** 510 * Checks to see if there is <i>anything</i> in the name, email, 511 * email confirmation, subject, and description. 512 * 513 * @return {@code true} if all of the required fields have some sort of 514 * input, {@code false} otherwise. 515 */ 516 private boolean validInput() { 517 if (userField.getText().isEmpty()) { 518 return false; 519 } 520 if (emailField.getText().isEmpty()) { 521 return false; 522 } 523 if (confirmField.getText().isEmpty()) { 524 return false; 525 } 526 if (subjectField.getText().isEmpty()) { 527 return false; 528 } 529 if (descriptionArea.getText().isEmpty()) { 530 return false; 531 } 532 return checkEmailAddresses(); 533 } 534 535 private void attachmentOneButtonActionPerformed(ActionEvent evt) { 536 attachFileToField(attachmentOneField); 537 } 538 539 private void attachmentTwoButtonActionPerformed(ActionEvent evt) { 540 attachFileToField(attachmentTwoField); 541 } 542 543 private void attachmentOneFieldMouseClicked(MouseEvent evt) { 544 if (attachmentOneField.getText().isEmpty()) { 545 attachFileToField(attachmentOneField); 546 } 547 } 548 549 private void attachmentTwoFieldMouseClicked(MouseEvent evt) { 550 if (attachmentTwoField.getText().isEmpty()) { 551 attachFileToField(attachmentTwoField); 552 } 553 } 554 555 private void showInvalidInputs() { 556 // how to display these? 557 JOptionPane.showMessageDialog(this, "You must provide at least your name, email address, subject, and description.", "Missing required input", JOptionPane.ERROR_MESSAGE); 558 } 559 560 private void sendRequest(ActionEvent evt) { 561 // check input validity 562 if (!validInput()) { 563 showInvalidInputs(); 564 return; 565 } 566 567 // disable the ability to send more requests until we get a status 568 // reply from the server. 569 if (sendButton != null) { 570 sendButton.setEnabled(false); 571 } 572 573 // persist things that need it. 574 persistInput(); 575 576 // create a background thread 577 listener.task = new Submitter(this); 578 579 // send the worker thread to the mines 580 exec.execute(listener.task); 581 } 582 583 /** 584 * Due to some fields persisting user input between McIDAS-V sessions we 585 * set the focus to be on the first of these fields <i>lacking</i> input. 586 */ 587 private void otherDoFocusThingNow() { 588 List<JTextComponent> comps = CollectionHelpers.list(userField, 589 emailField, confirmField, organizationField, subjectField, descriptionArea); 590 591 for (JTextComponent comp : comps) { 592 if (comp.getText().isEmpty()) { 593 comp.requestFocus(true); 594 break; 595 } 596 } 597 } 598 599 private static void attachFileToField(final JTextField field) { 600 String current = field.getText(); 601 JFileChooser jfc = new JFileChooser(current); 602 if (jfc.showOpenDialog(field) == JFileChooser.APPROVE_OPTION) { 603 field.setText(jfc.getSelectedFile().toString()); 604 } 605 } 606 607 private class CancelListener implements ActionListener { 608 BackgroundTask<?> task; 609 public void actionPerformed(ActionEvent e) { 610 if (task != null) { 611 task.cancel(true); 612 } 613 setVisible(false); 614 dispose(); 615 } 616 } 617 618 /** 619 * Launch a test of the Support Request Form. 620 * 621 * @param args Ignored. 622 */ 623 public static void main(String[] args) { 624 EventQueue.invokeLater(() -> { 625 try { 626 new SupportForm( 627 new IntegratedDataViewer().getStore(), 628 new SimpleStateCollector() 629 ).setVisible(true); 630 } catch (Exception e) { 631 logger.error("Problem creating support form", e); 632 } 633 }); 634 } 635}