001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2025
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.*;
064import java.nio.file.Files;
065import java.nio.file.Paths;
066import java.util.ArrayList;
067import java.util.List;
068import java.util.concurrent.ExecutorService;
069import java.util.concurrent.Executors;
070import java.util.zip.ZipEntry;
071import java.util.zip.ZipOutputStream;
072
073/**
074 * This class handles all the GUI elements of a McIDAS-V support request.
075 */
076public class SupportForm extends JFrame {
077    
078    private static final Logger logger =
079        LoggerFactory.getLogger(SupportForm.class);
080    
081    public static final String PROP_SUPPORTREQ_BUNDLE = "mcv.supportreq.bundle";
082    
083    public static final String PROP_SUPPORTREQ_CC = "mcv.supportreq.cc";
084    
085    public static final String PROP_SUPPORTREQ_CONFIRMEMAIL = "mcv.supportreq.confirmedemail";
086    
087    private static final String HELP_ID = "idv.tools.supportrequestform";
088    
089    private static ExecutorService exec = Executors.newCachedThreadPool();
090    
091    private final IdvObjectStore store;
092    
093    private final StateCollector collector;
094    
095    private final CancelListener listener = new CancelListener();
096    
097    private JPanel contentPane;
098    private JTextField userField;
099    private JTextField emailField;
100    private JTextField confirmField;
101    private JTextField organizationField;
102    private JTextField subjectField;
103    private JTextField attachmentOneField;
104    private JTextField attachmentTwoField;
105    private JTextArea descriptionArea;
106    private JCheckBox bundleCheckBox;
107    private JCheckBox ccCheckBox;
108    private JButton sendButton;
109    private JButton cancelButton;
110    private JButton helpButton;
111
112    // TODO: McIDAS Inquiry #3159-3141 -> get a list of all file types that the wisc.edu email server
113    // TODO: does not allow and add it here
114    private static String[] unsupportedAttachments = {"py", "bat"}; // add more here
115
116    private static ArrayList<String> filesToDelete = new ArrayList<>();
117    
118    /**
119     * Creates a support request form that collects information about
120     * the current McIDAS-V session.
121     * 
122     * @param store Storage for persisted user input. Should not be {@code null}.
123     * @param collector Collects information about the current session.
124     */
125    public SupportForm(IdvObjectStore store, StateCollector collector) {
126        this.store = requireNonNull(store);
127        this.collector = requireNonNull(collector);
128        initComponents();
129        unpersistInput();
130        otherDoFocusThingNow();
131    }
132    
133    /**
134     * Saves user input for the following: name, email address, email address
135     * confirmation, organization, whether or not to CC the user a copy, and 
136     * whether or not a {@literal "state"} bundle should be included.
137     * 
138     * <p>You should initialize the GUI components before calling this method.
139     */
140    private void persistInput() {
141        store.put(IdvUIManager.PROP_HELP_NAME, getUser());
142        store.put(IdvUIManager.PROP_HELP_EMAIL, getEmail());
143        store.put(PROP_SUPPORTREQ_CONFIRMEMAIL, getConfirmedEmail());
144        store.put(IdvUIManager.PROP_HELP_ORG, getOrganization());
145        store.put(PROP_SUPPORTREQ_CC, getSendCopy());
146        store.put(PROP_SUPPORTREQ_BUNDLE, getSendBundle());
147        store.save();
148    }
149    
150    /**
151     * Loads user input for the following: name, email address, email address
152     * confirmation, organization, whether or not to CC the user a copy, and 
153     * whether or not a {@literal "state"} bundle should be included.
154     * 
155     * <p>You should initialize the GUI components before calling this method.
156     */
157    private void unpersistInput() {
158        userField.setText(store.get(IdvUIManager.PROP_HELP_NAME, ""));
159        emailField.setText(store.get(IdvUIManager.PROP_HELP_EMAIL, ""));
160        confirmField.setText(store.get(PROP_SUPPORTREQ_CONFIRMEMAIL, ""));
161        organizationField.setText(store.get(IdvUIManager.PROP_HELP_ORG, ""));
162        ccCheckBox.setSelected(store.get(PROP_SUPPORTREQ_CC, true));
163        bundleCheckBox.setSelected(store.get(PROP_SUPPORTREQ_BUNDLE, false));
164    }
165    
166    /**
167     * Create the frame.
168     */
169    public void initComponents() {
170        setTitle("Request McIDAS-V Support");
171        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
172        setBounds(100, 100, 682, 538);
173        contentPane = new JPanel();
174        setContentPane(contentPane);
175        contentPane.setLayout(new MigLayout("", "[][grow]", "[][][][][][][grow][][][][][][]"));
176        
177        JLabel nameLabel = new JLabel("Your Name:");
178        contentPane.add(nameLabel, "cell 0 0,alignx right");
179        
180        userField = new JTextField();
181        contentPane.add(userField, "cell 1 0,growx");
182        userField.setName("user");
183        userField.setColumns(10);
184        
185        JLabel emailLabel = new JLabel("Your Email:");
186        contentPane.add(emailLabel, "cell 0 1,alignx right");
187        
188        emailField = new JTextField();
189        contentPane.add(emailField, "cell 1 1,growx");
190        emailField.setName("email");
191        emailField.setColumns(10);
192        
193        JLabel confirmLabel = new JLabel("Confirm Email:");
194        contentPane.add(confirmLabel, "cell 0 2,alignx right");
195        
196        confirmField = new JTextField();
197        contentPane.add(confirmField, "cell 1 2,growx");
198        confirmField.setName("confirm");
199        confirmField.setColumns(10);
200        
201        JLabel organizationLabel = new JLabel("Organization:");
202        contentPane.add(organizationLabel, "cell 0 3,alignx right");
203        
204        organizationField = new JTextField();
205        contentPane.add(organizationField, "cell 1 3,growx");
206        organizationField.setName("organization");
207        organizationField.setColumns(10);
208        
209        JLabel subjectLabel = new JLabel("Subject:");
210        contentPane.add(subjectLabel, "cell 0 4,alignx right");
211        
212        subjectField = new JTextField();
213        contentPane.add(subjectField, "cell 1 4,growx");
214        subjectField.setName("subject");
215        subjectField.setColumns(10);
216        
217        JLabel descriptiveLabel = new JLabel("Please provide a thorough description of the problem you encountered.");
218        contentPane.add(descriptiveLabel, "cell 1 5");
219        
220        JLabel descriptionLabel = new JLabel("Description:");
221        contentPane.add(descriptionLabel, "cell 0 6,alignx right,aligny top");
222        
223        descriptionArea = new JTextArea();
224        JScrollPane scrollPane = new JScrollPane(descriptionArea);
225        contentPane.add(scrollPane, "cell 1 6,grow");
226        descriptionArea.setName("description");
227        descriptionArea.setLineWrap(true);
228        descriptionArea.setWrapStyleWord(true);
229        descriptionArea.setColumns(20);
230        descriptionArea.setRows(6);
231        
232        JLabel attachmentOneLabel = new JLabel("Attachment 1:");
233        contentPane.add(attachmentOneLabel, "cell 0 7,alignx right");
234        
235        attachmentOneField = new JTextField();
236        attachmentOneField.addMouseListener(new MouseAdapter() {
237            @Override public void mouseClicked(MouseEvent evt) {
238                attachmentOneFieldMouseClicked(evt);
239            }
240        });
241        contentPane.add(attachmentOneField, "flowx,cell 1 7,growx");
242        attachmentOneField.setName("attachment1");
243        attachmentOneField.setColumns(10);
244        
245        JButton attachmentOneButton = new JButton("Browse...");
246        attachmentOneButton.addActionListener(new ActionListener() {
247            public void actionPerformed(ActionEvent evt) {
248                attachmentOneButtonActionPerformed(evt);
249            }
250        });
251        contentPane.add(attachmentOneButton, "cell 1 7,alignx left");
252        
253        JLabel attachmentTwoLabel = new JLabel("Attachment 2:");
254        contentPane.add(attachmentTwoLabel, "cell 0 8,alignx right");
255        
256        attachmentTwoField = new JTextField();
257        attachmentTwoField.addMouseListener(new MouseAdapter() {
258            @Override public void mouseClicked(MouseEvent evt) {
259                attachmentTwoFieldMouseClicked(evt);
260            }
261        });
262        contentPane.add(attachmentTwoField, "flowx,cell 1 8,growx");
263        attachmentTwoField.setName("attachment2");
264        attachmentTwoField.setColumns(10);
265        
266        JButton attachmentTwoButton = new JButton("Browse...");
267        attachmentTwoButton.addActionListener(new ActionListener() {
268            public void actionPerformed(ActionEvent evt) {
269                attachmentTwoButtonActionPerformed(evt);
270            }
271        });
272        contentPane.add(attachmentTwoButton, "cell 1 8,alignx left");
273        
274        bundleCheckBox = new JCheckBox("Include current application state as a bundle.");
275        bundleCheckBox.setName("sendstate");
276        contentPane.add(bundleCheckBox, "cell 1 9,alignx left");
277        
278        ccCheckBox = new JCheckBox("Send copy of support request to the email address I provided.");
279        ccCheckBox.setName("ccrequest");
280        ccCheckBox.setSelected(true);
281        contentPane.add(ccCheckBox, "cell 1 10,alignx left");
282        
283        helpButton = new JButton("Help");
284        helpButton.addActionListener(new ActionListener() {
285            public void actionPerformed(ActionEvent evt) {
286                ucar.unidata.ui.Help.getDefaultHelp().gotoTarget(HELP_ID);
287            }
288        });
289        contentPane.add(helpButton, "flowx,cell 1 12,alignx right");
290        
291        cancelButton = new JButton("Cancel");
292        cancelButton.addActionListener(listener);
293        contentPane.add(cancelButton, "cell 1 12,alignx right");
294        
295        sendButton = new JButton("Send Request");
296        sendButton.addActionListener(new ActionListener() {
297            public void actionPerformed(ActionEvent evt) {
298                sendRequest(evt);
299            }
300        });
301        contentPane.add(sendButton, "cell 1 12,alignx right");
302        contentPane.setFocusTraversalPolicy(new FocusTraveller(userField, emailField, confirmField, organizationField, subjectField, descriptionArea, attachmentOneButton, attachmentTwoButton, bundleCheckBox, ccCheckBox, helpButton, cancelButton, sendButton));
303    }
304    
305    /**
306     * Checks {@link #emailField} and {@link #confirmField} to see if they 
307     * match (case is ignored).
308     * 
309     * @return {@code true} if there is a match, {@code false} otherwise.
310     */
311    public boolean checkEmailAddresses() {
312        return emailField.getText().equalsIgnoreCase(confirmField.getText());
313    }
314    
315    /**
316     * Returns whatever occupies {@link #userField}.
317     * 
318     * @return User's name.
319     */
320    public String getUser() {
321        return userField.getText();
322    }
323    
324    /**
325     * Returns whatever currently lives in {@link #emailField}.
326     * 
327     * @return User's email address.
328     */
329    public String getEmail() {
330        return emailField.getText();
331    }
332    
333    /**
334     * Returns whatever currently lives in {@link #confirmField}.
335     * 
336     * @return User's confirmed email address.
337     */
338    public String getConfirmedEmail() {
339        return confirmField.getText();
340    }
341    
342    /**
343     * Returns whatever resides in {@link #subjectField}.
344     * 
345     * @return Subject of the support request.
346     */
347    public String getSubject() {
348        return subjectField.getText();
349    }
350    
351    /**
352     * Returns whatever has commandeered {@link #organizationField}.
353     * 
354     * @return Organization to which the user belongs.
355     */
356    public String getOrganization() {
357        return organizationField.getText();
358    }
359    
360    /**
361     * Returns whatever is ensconced inside {@link #descriptionArea}.
362     * 
363     * @return Body of the user's email.
364     */
365    public String getDescription() {
366        return descriptionArea.getText();
367    }
368    
369    /**
370     * Checks whether or not the user has attached a file in the 
371     * {@literal "first file"} slot.
372     * 
373     * @return {@code true} if there's a file, {@code false} otherwise.
374     */
375    public boolean hasAttachmentOne() {
376        return new File(attachmentOneField.getText()).exists();
377    }
378    
379    /**
380     * Checks whether or not the user has attached a file in the 
381     * {@literal "second file"} slot.
382     * 
383     * @return {@code true} if there's a file, {@code false} otherwise.
384     */
385    public boolean hasAttachmentTwo() {
386        return new File(attachmentTwoField.getText()).exists();
387    }
388    
389    /**
390     * Returns whatever file path has monopolized {@link #attachmentOneField}.
391     * 
392     * @return Path to the first file attachment, or a blank string if no file
393     * has been selected.
394     */
395    public String getAttachmentOne() {
396        return attachmentOneField.getText();
397    }
398    
399    /**
400     * Returns whatever file path has appeared within 
401     * {@link #attachmentTwoField}.
402     * 
403     * @return Path to the second file attachment, or a blank string if no 
404     * file has been selected.
405     */
406    public String getAttachmentTwo() {
407        return attachmentTwoField.getText();
408    }
409    
410    // TODO: javadocs!
411    public boolean getSendCopy() {
412        return ccCheckBox.isSelected();
413    }
414    
415    public boolean getSendBundle() {
416        return bundleCheckBox.isSelected();
417    }
418    
419    public byte[] getExtraState() {
420        return collector.getContents();
421    }
422    
423    public String getExtraStateName() {
424        return collector.getExtraAttachmentName();
425    }
426    
427    public boolean canBundleState() {
428        return collector.canBundleState();
429    }
430    
431    public byte[] getBundledState() {
432        return collector.getBundledState();
433    }
434    
435    public String getBundledStateName() {
436        return collector.getBundleAttachmentName();
437    }
438    
439    /**
440     * Determine if {@code mcidasv.log} can be sent to the Help Desk.
441     *
442     * @return {@code true} if {@code mcidasv.log} exists, {@code false}
443     * otherwise.
444     */
445    public boolean canSendLog() {
446        String path = collector.getLogPath();
447        if (path == null || path.isEmpty()) {
448            return false;
449        }
450        return new File(path).exists();
451    }
452    
453    /**
454     * Get path to where {@code mcidasv.log} <b>should</b> be located.
455     *
456     * @return String representing the full path to the user's
457     * {@code mcidasv.log}. Note: <b>the file may not exist!</b>
458     */
459    public String getLogPath() {
460        return collector.getLogPath();
461    }
462    
463    /**
464     * Determine if {@code RESOLV.SRV} can be sent to the Help Desk.
465     *
466     * @return {@code true} if {@code RESOLV.SRV} exists, {@code false}
467     * otherwise.
468     */
469    public boolean canSendResolvSrv() {
470        String path = collector.getResolvSrvPath();
471        boolean result = false;
472        if ((path != null) && !path.isEmpty()) {
473            result = Files.exists(Paths.get(path));
474        }
475        return result;
476    }
477    
478    /**
479     * Get the path to where the user's {@code RESOLV.SRV} file <b>should</b>
480     * be located.
481     *
482     * @return Path to {@code RESOLV.SRV}. Note: <b>the file may not exist!</b>
483     */
484    public String getResolvSrvPath() {
485        return collector.getResolvSrvPath();
486    }
487    
488    /**
489     * Get path to McV prefs file.
490     *
491     * @return
492     */
493    public String getPrefsPath() {
494        return collector.getPrefsPath();
495    }
496    
497    // TODO: dialogs are bad news bears.
498    public void showSuccess() {
499        setVisible(false);
500        dispose();
501        JOptionPane.showMessageDialog(null, "Support request sent successfully.", "Success", JOptionPane.DEFAULT_OPTION);
502        // delete compressed files, if any exist
503        clearFiles();
504    }
505    
506    // TODO: dialogs are bad news hares.
507    public void showFailure(final String reason) {
508        clearFiles();
509        String msg = "";
510        if (reason == null || reason.isEmpty()) {
511            msg = "Error sending request, could not determine cause.";
512        } else {
513            msg = "Error sending request:\n"+reason;
514        }
515        JOptionPane.showMessageDialog(this, msg, "Problem sending support request", JOptionPane.ERROR_MESSAGE);
516        if (sendButton != null) {
517            sendButton.setEnabled(true);
518        }
519    }
520    
521    /**
522     * Checks to see if there is <i>anything</i> in the name, email, 
523     * email confirmation, subject, and description.
524     * 
525     * @return {@code true} if all of the required fields have some sort of 
526     * input, {@code false} otherwise.
527     */
528    private boolean validInput() {
529        if (userField.getText().isEmpty()) {
530            return false;
531        }
532        if (emailField.getText().isEmpty()) {
533            return false;
534        }
535        if (confirmField.getText().isEmpty()) {
536            return false;
537        }
538        if (subjectField.getText().isEmpty()) {
539            return false;
540        }
541        if (descriptionArea.getText().isEmpty()) {
542            return false;
543        }
544        return checkEmailAddresses();
545    }
546    
547    private void attachmentOneButtonActionPerformed(ActionEvent evt) {
548        attachFileToField(attachmentOneField);
549    }
550    
551    private void attachmentTwoButtonActionPerformed(ActionEvent evt) {
552        attachFileToField(attachmentTwoField);
553    }
554    
555    private void attachmentOneFieldMouseClicked(MouseEvent evt) {
556        if (attachmentOneField.getText().isEmpty()) {
557            attachFileToField(attachmentOneField);
558        }
559    }
560    
561    private void attachmentTwoFieldMouseClicked(MouseEvent evt) {
562        if (attachmentTwoField.getText().isEmpty()) {
563            attachFileToField(attachmentTwoField);
564        }
565    }
566    
567    private void showInvalidInputs() {
568        // how to display these?
569        JOptionPane.showMessageDialog(this, "You must provide at least your name, email address, subject, and description.", "Missing required input", JOptionPane.ERROR_MESSAGE);
570    }
571    
572    private void sendRequest(ActionEvent evt) {
573
574        // check input validity
575        if (!validInput()) {
576            showInvalidInputs();
577            return;
578        }
579
580        // zip files!
581        if (!(zipFile(attachmentOneField) && zipFile(attachmentTwoField))) return;
582        
583        // disable the ability to send more requests until we get a status
584        // reply from the server.
585        if (sendButton != null) {
586            sendButton.setEnabled(false);
587        }
588        
589        // persist things that need it.
590        persistInput();
591        
592        // create a background thread
593        listener.task = new Submitter(this);
594        
595        // send the worker thread to the mines
596        exec.execute(listener.task);
597    }
598    
599    /**
600     * Due to some fields persisting user input between McIDAS-V sessions we
601     * set the focus to be on the first of these fields <i>lacking</i> input.
602     */
603    private void otherDoFocusThingNow() {
604        List<JTextComponent> comps = CollectionHelpers.list(userField, 
605            emailField, confirmField, organizationField, subjectField, descriptionArea);
606        
607        for (JTextComponent comp : comps) {
608            if (comp.getText().isEmpty()) {
609                comp.requestFocus(true);
610                break;
611            }
612        }
613    }
614
615    public static void clearFiles() {
616        for (int i = 0; i < filesToDelete.size(); i++) {
617            logger.warn(filesToDelete.get(i) + " will be deleted!");
618            File toDelete = new File(filesToDelete.get(i));
619            toDelete.delete();
620        }
621    }
622
623    private static void attachFileToField(final JTextField field) {
624        String current = field.getText();
625        JFileChooser jfc = new JFileChooser(current);
626        if (jfc.showOpenDialog(field) == JFileChooser.APPROVE_OPTION) {
627            field.setText(jfc.getSelectedFile().toString());
628        }
629    }
630
631    private static String getFileExtensionSF(String fullName) {
632        String fileName = new File(fullName).getName();
633        int dotIndex = fileName.lastIndexOf('.');
634        return (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1);
635    }
636
637    private static String getFileNameWithoutExt(File file) {
638       String tmp = file.getName();
639       String[] arrTmp = tmp.split("\\.");
640       String out = "";
641
642       for (int i = 0; i < arrTmp.length - 1; i++) {
643           out += arrTmp[i];
644       }
645       return out;
646    }
647
648
649    /**
650     * McIDAS Inquiry #1690-3141
651     * Zip files before sending them through the support form
652     */
653    private static boolean zipFile(final JTextField field) {
654        String path = field.getText();
655        if (path.isEmpty() || path == null) {
656            // no file in the field, that's okay!
657            return true;
658        }
659        File file = new File(path);
660        String ext = getFileExtensionSF(path);
661
662        for (String e : unsupportedAttachments) {
663            logger.info(ext + " " + e + " " +ext.equalsIgnoreCase(e));
664            if (ext.equalsIgnoreCase(e)) {
665                JOptionPane.showMessageDialog(null, file.getName() + " may not be attatched correctly!", "Potentially dangerous file!", JOptionPane.ERROR_MESSAGE);
666                // let it pass
667            }
668        }
669
670        String newName = getFileNameWithoutExt(file) + "_" + ext + "File";
671        try {
672            FileInputStream inStream = new FileInputStream(file);
673
674            String newFilePath = newName + "_compressed.zip";
675            String newFileName = newName + "_compressed.zip";
676
677            FileOutputStream outStream = new FileOutputStream(newFilePath);
678            ZipOutputStream zipStream = new ZipOutputStream(outStream);
679
680            ZipEntry zipEntry = new ZipEntry(file.getName());
681            zipStream.putNextEntry(zipEntry);
682
683            byte[] bytes = new byte[1024];
684            int length;
685            while ((length = inStream.read(bytes)) >= 0) {
686                zipStream.write(bytes, 0, length);
687            }
688
689            zipStream.close();
690            inStream.close();
691            outStream.close();
692
693            filesToDelete.add(newFilePath);
694            field.setText(newFilePath);
695            logger.info("1: " + newFilePath);
696            logger.info("2: " + newFileName);
697            return true;
698        } catch (Exception e) {
699            logger.error("Zipping the file failed", e);
700            JOptionPane.showMessageDialog(null, "Unable to attach " + file.getName() + " file", "File Error", JOptionPane.ERROR_MESSAGE);
701            return false;
702        }
703    }
704
705    private class CancelListener implements ActionListener {
706        BackgroundTask<?> task;
707        public void actionPerformed(ActionEvent e) {
708            if (task != null) {
709                task.cancel(true);
710            }
711            setVisible(false);
712            dispose();
713        }
714    }
715    
716    /**
717     * Launch a test of the Support Request Form.
718     * 
719     * @param args Ignored.
720     */
721    public static void main(String[] args) {
722        EventQueue.invokeLater(()  -> {
723            try {
724                new SupportForm(
725                    new IntegratedDataViewer().getStore(),
726                    new SimpleStateCollector()
727                ).setVisible(true);
728            } catch (Exception e) {
729                logger.error("Problem creating support form", e);
730            }
731        });
732    }
733}