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}