001package jmri.jmrit.mailreport;
002
003import jmri.util.startup.PerformFileModel;
004import jmri.util.startup.StartupActionsManager;
005
006import java.awt.FlowLayout;
007import java.io.File;
008import java.io.FileInputStream;
009import java.io.FileNotFoundException;
010import java.io.FileOutputStream;
011import java.io.IOException;
012import java.nio.charset.StandardCharsets;
013import java.util.*;
014import java.util.zip.ZipEntry;
015import java.util.zip.ZipOutputStream;
016
017import javax.mail.internet.AddressException;
018import javax.mail.internet.InternetAddress;
019import javax.swing.BoxLayout;
020import javax.swing.JButton;
021import javax.swing.JCheckBox;
022import javax.swing.JLabel;
023import javax.swing.JPanel;
024import javax.swing.JTextArea;
025import javax.swing.JTextField;
026
027import jmri.InstanceManager;
028import jmri.profile.Profile;
029import jmri.profile.ProfileManager;
030import jmri.util.MultipartMessage;
031import jmri.util.swing.JmriJOptionPane;
032import jmri.util.javaworld.GridLayout2;
033import jmri.util.problemreport.LogProblemReportProvider;
034
035/**
036 * User interface for sending a problem report via email.
037 * <p>
038 * The report is sent to a dedicated SourceForge mailing list, from which people
039 * can retrieve it.
040 *
041 * @author Bob Jacobsen Copyright (C) 2009
042 * @author Matthew Harris Copyright (c) 2014
043 */
044public class ReportPanel extends JPanel {
045
046    JButton sendButton;
047    JTextField emailField = new JTextField(40);
048    JTextField summaryField = new JTextField(40);
049    JTextArea descField = new JTextArea(8, 40);
050    JCheckBox checkContext;
051    JCheckBox checkNetwork;
052    JCheckBox checkLog;
053    JCheckBox checkPanel;
054    JCheckBox checkProfile;
055    JCheckBox checkCopy;
056
057    // Define which profile sub-directories to include
058    // In lowercase as I was too lazy to do a proper case-insensitive check...
059    String[] profDirs = {"networkservices", "profile", "programmers", "throttle"};
060
061    public ReportPanel() {
062        ResourceBundle rb = java.util.ResourceBundle.getBundle("jmri.jmrit.mailreport.ReportBundle");
063
064        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
065
066        JPanel p1;
067
068        p1 = new JPanel();
069        p1.setLayout(new FlowLayout());
070        p1.add(new JLabel(rb.getString("LabelTop")));
071        add(p1);
072
073        // grid of options
074        p1 = new JPanel();
075        p1.setLayout(new GridLayout2(3, 2));
076        add(p1);
077
078        JLabel l = new JLabel(rb.getString("LabelEmail"));
079        l.setToolTipText(rb.getString("TooltipEmail"));
080        p1.add(l);
081        emailField.setToolTipText(rb.getString("TooltipEmail"));
082        p1.add(emailField);
083
084        l = new JLabel(rb.getString("LabelSummary"));
085        l.setToolTipText(rb.getString("TooltipSummary"));
086        p1.add(l);
087        summaryField.setToolTipText(rb.getString("TooltipSummary"));
088        p1.add(summaryField);
089
090        l = new JLabel(rb.getString("LabelDescription"));
091        p1.add(l);
092        // This ensures that the long-description JTextArea font
093        // is the same as the JTextField fields.
094        // With some L&F, default font for JTextArea differs.
095        descField.setFont(summaryField.getFont());
096        descField.setBorder(summaryField.getBorder());
097        descField.setLineWrap(true);
098        descField.setWrapStyleWord(true);
099        p1.add(descField);
100
101        // buttons on bottom
102        p1 = new JPanel();
103        p1.setLayout(new FlowLayout());
104        checkContext = new JCheckBox(rb.getString("CheckContext"));
105        checkContext.setSelected(true);
106        checkContext.addActionListener(new java.awt.event.ActionListener() {
107            @Override
108            public void actionPerformed(java.awt.event.ActionEvent e) {
109                checkNetwork.setEnabled(checkContext.isSelected());
110            }
111        });
112        p1.add(checkContext);
113
114        checkNetwork = new JCheckBox(rb.getString("CheckNetwork"));
115        checkNetwork.setSelected(true);
116        p1.add(checkNetwork);
117
118        checkLog = new JCheckBox(rb.getString("CheckLog"));
119        checkLog.setSelected(true);
120        p1.add(checkLog);
121        add(p1);
122
123        p1 = new JPanel();
124        p1.setLayout(new FlowLayout());
125        checkPanel = new JCheckBox(rb.getString("CheckPanel"));
126        checkPanel.setSelected(true);
127        p1.add(checkPanel);
128
129        checkProfile = new JCheckBox(rb.getString("CheckProfile"));
130        checkProfile.setSelected(true);
131        p1.add(checkProfile);
132
133        checkCopy = new JCheckBox(rb.getString("CheckCopy"));
134        checkCopy.setSelected(true);
135        p1.add(checkCopy);
136        add(p1);
137
138        sendButton = new javax.swing.JButton(rb.getString("ButtonSend"));
139        sendButton.setToolTipText(rb.getString("TooltipSend"));
140        sendButton.addActionListener(new java.awt.event.ActionListener() {
141            @Override
142            public void actionPerformed(java.awt.event.ActionEvent e) {
143                sendButtonActionPerformed(e);
144            }
145        });
146        add(sendButton);
147    }
148
149    // made static, public, not final so can be changed via script
150    static public String requestURL = "http://jmri.org/problem-report.php";  // NOI18N
151
152    public void sendButtonActionPerformed(java.awt.event.ActionEvent e) {
153        ResourceBundle rb = ResourceBundle.getBundle("jmri.jmrit.mailreport.ReportBundle");
154        try {
155            sendButton.setEnabled(false);
156            log.debug("initial checks");
157            InternetAddress email = new InternetAddress(emailField.getText());
158            email.validate();
159
160            log.debug("start send");
161
162            MultipartMessage msg = new MultipartMessage(requestURL, StandardCharsets.UTF_8.name());
163
164            // add reporter email address
165            log.debug("start creating message");
166            msg.addFormField("reporter", emailField.getText());
167
168            // add if to Cc sender
169            msg.addFormField("sendcopy", checkCopy.isSelected() ? "yes" : "no");
170
171            // add problem summary
172            msg.addFormField("summary", summaryField.getText());
173
174            // build detailed error report (include context if selected)
175            String report = descField.getText() + "\r\n";
176            if (checkContext.isSelected()) {
177                report += "=========================================================\r\n"; // NOI18N
178                report += (new ReportContext()).getReport(checkNetwork.isSelected() && checkNetwork.isEnabled());
179            }
180            msg.addFormField("problem", report);
181
182            log.debug("start adding attachments");
183            // add panel file if OK
184            if (checkPanel.isSelected()) {
185                log.debug("prepare panel attachment");
186                // Check that some startup panel files have been loaded
187                for (PerformFileModel m : InstanceManager.getDefault(StartupActionsManager.class).getActions(PerformFileModel.class)) {
188                    String fn = m.getFileName();
189                    File f = new File(fn);
190                    log.info("Add panel file loaded at startup: {}", f);
191                    msg.addFilePart("logfileupload[]", f);
192                }
193                // Check that a manual panel file has been loaded
194                File file = jmri.configurexml.LoadXmlUserAction.getCurrentFile();
195                if (file != null) {
196                    log.info("Adding manually-loaded panel file: {}", file.getPath());
197                    msg.addFilePart("logfileupload[]", jmri.configurexml.LoadXmlUserAction.getCurrentFile());
198                } else {
199                    // No panel file loaded by manual action
200                    log.debug("No panel file manually loaded");
201                }
202            }
203
204            // add profile files if OK
205            if (checkProfile.isSelected()) {
206                log.debug("prepare profile attachment");
207                // Check that a profile has been loaded
208                Profile profile = ProfileManager.getDefault().getActiveProfile();
209                if (profile != null) {
210                    File file = profile.getPath();
211                    if (file != null) {
212                        log.debug("add profile: {}", file.getPath());
213                        // Now zip-up contents of profile
214                        // Create temp file that will be deleted when Java quits
215                        File temp = File.createTempFile("profile", ".zip");
216                        temp.deleteOnExit();
217
218                        FileOutputStream out = new FileOutputStream(temp);
219                        ZipOutputStream zip = new ZipOutputStream(out);
220
221                        addDirectory(zip, file);
222
223                        zip.close();
224                        out.close();
225
226                        msg.addFilePart("logfileupload[]", temp);
227                    }
228                } else {
229                    // No profile loaded
230                    log.warn("No profile loaded - not sending");
231                }
232            }
233
234            // add the log if OK
235            if (checkLog.isSelected()) {
236                log.debug("prepare log attachments");
237                ServiceLoader<LogProblemReportProvider> loggers = ServiceLoader.load(LogProblemReportProvider.class);
238                for(LogProblemReportProvider provider : loggers) {
239                    for(File file : provider.getFiles()) {
240                        msg.addFilePart("logfileupload[]", file, "application/octet-stream");
241                    }
242                }
243                loggers.reload(); // allow garbage collection of loaders
244            }
245            log.debug("done adding attachments");
246
247            // finalise and get server response (if any)
248            log.debug("posting report...");
249            List<String> response = msg.finish();
250            log.debug("send complete");
251            log.debug("server response:");
252            boolean checkResponse = false;
253            for (String line : response) {
254                log.debug("         line: {}", line);
255                if (line.contains("<p>Message successfully sent!</p>")) {
256                    checkResponse = true;
257                }
258            }
259
260            if (checkResponse) {
261                JmriJOptionPane.showMessageDialog(this, rb.getString("InfoMessage"),
262                    rb.getString("InfoTitle"), JmriJOptionPane.INFORMATION_MESSAGE);
263                // close containing Frame
264                getTopLevelAncestor().setVisible(false);
265            } else {
266                JmriJOptionPane.showMessageDialog(this, rb.getString("ErrMessage"),
267                    rb.getString("ErrTitle"), JmriJOptionPane.ERROR_MESSAGE); // TODO add Bundle to folder and use ErrorTitle key in NamedBeanBundle props
268                sendButton.setEnabled(true);
269            }
270
271        } catch (IOException ex) {
272            log.error("Error when attempting to send report", ex);
273            sendButton.setEnabled(true);
274        } catch (AddressException ex) {
275            log.error("Invalid email address", ex);
276            JmriJOptionPane.showMessageDialog(this, rb.getString("ErrAddress"),
277                rb.getString("ErrTitle"), JmriJOptionPane.ERROR_MESSAGE); // TODO add Bundle to folder and use ErrorTitle key in NamedBeanBundle props
278            sendButton.setEnabled(true);
279        }
280    }
281
282    private void addDirectory(ZipOutputStream out, File source) {
283        log.debug("Add profile: {}", source.getName());
284        addDirectory(out, source, "");
285    }
286
287    private void addDirectory(ZipOutputStream out, File source, String directory) {
288        // get directory contents
289        File[] files = source.listFiles();
290
291        log.debug("Add directory: {}", directory);
292        if ( files == null ) {
293            log.warn("No files in directory {}",source);
294            return;
295        }
296
297        for (File file : files) {
298            // if current file is a directory, call recursively
299            if (file.isDirectory()) {
300                // Only include certain sub-directories
301                if (!directory.equals("") || Arrays.asList(profDirs).contains(file.getName().toLowerCase())) {
302                    try {
303                        out.putNextEntry(new ZipEntry(directory + file.getName() + "/"));
304                    } catch (IOException ex) {
305                        log.error("Exception when adding directory", ex);
306                    }
307                    addDirectory(out, file, directory + file.getName() + "/");
308                } else {
309                    log.debug("Skipping: {}{}", directory, file.getName());
310                }
311                continue;
312            }
313            // Got here - add file
314            try {
315                // Only include certain files
316                if (!directory.equals("") || file.getName().toLowerCase().matches(".*(config\\.xml|\\.properties)")) {
317                    log.debug("Add file: {}{}", directory, file.getName());
318                    byte[] buffer = new byte[1024];
319                    try (FileInputStream in = new FileInputStream(file)) {
320                        out.putNextEntry(new ZipEntry(directory + file.getName()));
321
322                        int length;
323                        while ((length = in.read(buffer)) > 0) {
324                            out.write(buffer, 0, length);
325                        }
326                        out.closeEntry();
327                        in.close();
328                    }
329                } else {
330                    log.debug("Skip file: {}{}", directory, file.getName());
331                }
332            } catch (FileNotFoundException ex) {
333                log.error("Exception when adding file", ex);
334            } catch (IOException ex) {
335                log.error("Exception when adding file", ex);
336            }
337        }
338    }
339
340    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ReportPanel.class);
341
342}