package org.freeplane.plugin.bugreport; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.security.MessageDigest; import java.text.SimpleDateFormat; import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.StreamHandler; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JOptionPane; import org.freeplane.core.resources.ResourceController; import org.freeplane.core.util.FreeplaneVersion; import org.freeplane.core.util.HtmlUtils; import org.freeplane.core.util.LogUtils; import org.freeplane.core.util.TextUtils; import org.freeplane.features.mode.Controller; import org.freeplane.features.ui.ViewController; public class ReportGenerator extends StreamHandler { private static final String BUGREPORT_USER_ID = "org.freeplane.plugin.bugreport.userid"; private static final String REMOTE_LOG = "RemoteLog"; private static final String NO_REPORTS_SENT_BEFORE = "no reports sent before"; static final String LAST_BUG_REPORT_INFO = "last_bug_report_info"; private class SubmitRunner implements Runnable { public SubmitRunner() { } public void run() { runSubmit(); } } private class SubmitStarter implements Runnable { SubmitStarter() { if (EventQueue.isDispatchThread()) { return; } final Thread currentThread = Thread.currentThread(); EventQueue.invokeLater(new Runnable() { public void run() { try { currentThread.join(1000); } catch (final InterruptedException e) { } } }); } public void run() { startSubmit(); } } private final static String BUG_TRACKER_REFERENCE_URL = "http://freeplane.sourceforge.net/info/bugtracker.ref.txt"; private static String BUG_TRACKER_URL = null; static boolean disabled = false; private static int errorCounter = 0; private static String info; static final private String OPTION = "org.freeplane.plugin.bugreport"; private static ByteArrayOutputStream out = null; private static String version; private static String revision; private static String toHexString(final byte[] v) { final String HEX_DIGITS = "0123456789abcdef"; final StringBuffer sb = new StringBuffer(v.length * 2); for (int i = 0; i < v.length; i++) { final int b = v[i] & 0xFF; sb.append(HEX_DIGITS.charAt(b >>> 4)).append(HEX_DIGITS.charAt(b & 0xF)); } return sb.toString(); } private String hash = null; private boolean isRunning; private String log = null; private MessageDigest md = null; private boolean reportCollected = false; private IBugReportListener bugReportListener; public IBugReportListener getBugReportListener() { return bugReportListener; } public void setBugReportListener(final IBugReportListener bugReportListener) { this.bugReportListener = bugReportListener; } public ReportGenerator() { super(); try { setEncoding("UTF-8"); } catch (final SecurityException e) { } catch (final UnsupportedEncodingException e) { } setFormatter(new BugFormatter()); setLevel(Level.SEVERE); } private String calculateHash(final String errorMessage) { final String[] lines = errorMessage.split("\n"); final StringBuffer hashInput = new StringBuffer(); for (int i = 0; i < lines.length; i++) { final String s = lines[i]; if (s.startsWith("\tat org.freeplane.") || s.startsWith("missing key ") ) { hashInput.append(s); } } if (hashInput.length() == 0) { return null; } hashInput.append(version); hashInput.append(revision); try { return calculateHash(hashInput.toString().getBytes(getEncoding())); } catch (final UnsupportedEncodingException e) { return null; } } private String calculateHash(final byte[] byteArray) { try { if (md == null) { md = MessageDigest.getInstance("MD5"); } final byte[] digest = md.digest(byteArray); return ReportGenerator.toHexString(digest); } catch (final Exception e) { LogUtils.warn(e); return null; } } private void createInfo() { if (info == null) { final StringBuilder sb = new StringBuilder(); sb.append("freeplane_version = "); version = FreeplaneVersion.getVersion().toString(); sb.append(version); sb.append("; freeplane_xml_version = "); sb.append(FreeplaneVersion.XML_VERSION); revision = FreeplaneVersion.getVersion().getRevision(); if(! revision.equals("")){ sb.append("\nbzr revision = "); sb.append(revision); } sb.append("\njava_version = "); sb.append(System.getProperty("java.version")); sb.append("; os_name = "); sb.append(System.getProperty("os.name")); sb.append("; os_version = "); sb.append(System.getProperty("os.version")); sb.append('\n'); info = sb.toString(); } } private String getBugTrackerUrl() { if (BUG_TRACKER_URL != null) { return BUG_TRACKER_URL; } try { final URL url = new URL(BUG_TRACKER_REFERENCE_URL); final BufferedReader in = new BufferedReader(new InputStreamReader(url.openConnection().getInputStream())); BUG_TRACKER_URL = in.readLine(); return BUG_TRACKER_URL; } catch (final Exception e) { disabled = true; return null; } } private static class LogOpener implements ActionListener{ public void actionPerformed(ActionEvent e) { final String freeplaneLogDirectoryPath = LogUtils.getLogDirectory(); final File file = new File(freeplaneLogDirectoryPath); if(file.isDirectory()){ final ViewController viewController = Controller.getCurrentController().getViewController(); try { viewController.openDocument(file.toURL()); } catch (Exception ex) { } } } } JButton logButton; @Override public synchronized void publish(final LogRecord record) { if (out == null) { out = new ByteArrayOutputStream(); setOutputStream(out); } if (!isLoggable(record)) { return; } if (!(disabled || isRunning || reportCollected)) { reportCollected = true; EventQueue.invokeLater(new SubmitStarter()); } EventQueue.invokeLater(new Runnable() { @SuppressWarnings("serial") public void run() { errorCounter++; if(TextUtils.getRawText("internal_error_tooltip", null) != null){ if(logButton == null){ final ImageIcon errorIcon = new ImageIcon(ResourceController.getResourceController().getResource( "/images/icons/messagebox_warning.png")); logButton = new JButton(){ @Override public Dimension getPreferredSize(){ Dimension preferredSize = super.getPreferredSize(); preferredSize.height = getIcon().getIconHeight(); return preferredSize; } }; logButton.addActionListener(new LogOpener()); logButton.setIcon(errorIcon); String tooltip = TextUtils.getText("internal_error_tooltip"); logButton.setToolTipText(tooltip); Controller.getCurrentController().getViewController().addStatusComponent("internal_error", logButton); } logButton.setText(TextUtils.format("errornumber", errorCounter)); } } }); super.publish(record); } private void runSubmit() { try { close(); final String errorMessage = out.toString(getEncoding()); if (errorMessage.indexOf(getClass().getPackage().getName()) != -1) { // avoid infinite loops System.err.println("don't send bug reports from bugreport plugin"); return; } createInfo(); hash = calculateHash(errorMessage); if (hash == null) { return; } final String reportHeader = createReportHeader(); StringBuilder sb = new StringBuilder(); sb.append(reportHeader).append('\n').append("previous report : "); String lastReportInfo = ResourceController.getResourceController().getProperty(LAST_BUG_REPORT_INFO, NO_REPORTS_SENT_BEFORE); sb.append(lastReportInfo).append('\n'); final String userId = ResourceController.getResourceController().getProperty(BUGREPORT_USER_ID); if (userId.length() > 0){ sb.append("user : ").append(userId).append('\n'); } sb.append(info); sb.append(errorMessage); log = sb.toString(); if (log.equals("")) { return; } final ReportRegistry register = ReportRegistry.getInstance(); if (register.isReportRegistered(hash)) { return; } final String option = showBugReportDialog(); if (BugReportDialogManager.ALLOWED.equals(option)) { register.registerReport(hash, reportHeader); final Map<String, String> report = new LinkedHashMap<String, String>(); report.put("hash", hash); report.put("log", log); report.put("version", version); report.put("revision", revision); final String status = sendReport(report); if (bugReportListener == null || status == null) { return; } bugReportListener.onReportSent(report, status); } } catch (final UnsupportedEncodingException e) { LogUtils.severe(e); } finally { out = null; reportCollected = false; isRunning = false; } } private String createReportHeader() { SimpleDateFormat dateFormatGmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); dateFormatGmt.setTimeZone(TimeZone.getTimeZone("GMT")); String time = dateFormatGmt.format(new Date()); final String currentReportInfo = "at " + time + " CMT, hash " + hash; return currentReportInfo; } private String showBugReportDialog() { String option = ResourceController.getResourceController().getProperty(OPTION, BugReportDialogManager.ASK); if (option.equals(BugReportDialogManager.ASK)) { if(FreeplaneVersion.getVersion().isFinal()) return BugReportDialogManager.DENIED; String question = TextUtils.getText("org.freeplane.plugin.bugreport.question"); if (!question.startsWith("<html>")) { question = HtmlUtils.plainToHTML(question); } final Object[] options = new Object[] { TextUtils.getText("org.freeplane.plugin.bugreport.always_agree"), TextUtils.getText("org.freeplane.plugin.bugreport.agree"), TextUtils.getText("org.freeplane.plugin.bugreport.deny"), TextUtils.getText("org.freeplane.plugin.bugreport.always_deny") }; final String title = TextUtils.getText("org.freeplane.plugin.bugreport.dialog.title"); final String reportName = TextUtils.getText("org.freeplane.plugin.bugreport.report"); final int choice = BugReportDialogManager.showBugReportDialog(title, question, JOptionPane.INFORMATION_MESSAGE, options, options[1], reportName, log); switch (choice) { case 0: option = BugReportDialogManager.ALLOWED; ResourceController.getResourceController().setProperty(OPTION, option); break; case 1: option = BugReportDialogManager.ALLOWED; break; case 2: option = BugReportDialogManager.DENIED; break; case 3: option = BugReportDialogManager.DENIED; ResourceController.getResourceController().setProperty(OPTION, option); break; default: option = BugReportDialogManager.DENIED; break; } } return option; } private String sendReport(final Map<String, String> reportFields) { try { // Construct data final StringBuilder data = new StringBuilder(); for (final Entry<String, String> entry : reportFields.entrySet()) { if (data.length() != 0) { data.append('&'); } data.append(URLEncoder.encode(entry.getKey(), "UTF-8")); data.append('='); data.append(URLEncoder.encode(entry.getValue(), "UTF-8")); } // Send data final URL url = new URL(getBugTrackerUrl()); final URLConnection conn = url.openConnection(); conn.setDoOutput(true); final OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); wr.write(data.toString()); wr.flush(); // Get the response final BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); final String line = rd.readLine(); if (line != null) { System.out.println(line); } wr.close(); rd.close(); return line; } catch (final Exception e) { } return null; } private void startSubmit() { isRunning = true; final Thread submitterThread = new Thread(new SubmitRunner(), REMOTE_LOG); submitterThread.start(); } }