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}