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