/* This file is part of leafdigital leafChat. leafChat is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. leafChat is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with leafChat. If not, see <http://www.gnu.org/licenses/>. Copyright 2012 Samuel Marshall. */ package com.leafdigital.ui; import java.io.*; import java.net.*; import java.util.regex.*; import javax.swing.UIManager; import util.*; import util.xml.*; import com.leafdigital.prefs.api.*; import com.leafdigital.ui.api.*; import leafchat.core.api.*; /** User interface plugin */ @UIHandler({"errordialog", "license"}) public class UIPlugin implements Plugin { final static String PREFGROUP_MAINWINDOW="mainwindow", PREF_X="x",PREFDEFAULT_X="-1", PREF_Y="y",PREFDEFAULT_Y="-1", PREF_WIDTH="width",PREFDEFAULT_WIDTH="800", PREF_HEIGHT="height",PREFDEFAULT_HEIGHT="600", PREF_AGREEDLICENSE="agreed-license"; /** Context stored */ private PluginContext context; /** License dialog */ private Dialog license; /** License text */ public TextView licenseTextUI; /** Error dialog */ private Dialog errorDialog=null; /** Error message */ public TextView errorMessageUI; /** Report checkbox */ public CheckBox reportUI; /** Information about reporting. */ public Label reportInfoUI; private ErrorReportThread reporter=null; @Override public void init(PluginContext pc, PluginLoadReporter plr) throws GeneralException { this.context=pc; UIManager.put("DesktopPaneUI", "javax.swing.plaf.basic.BasicDesktopPaneUI"); UISingleton uis=new UISingleton(pc); pc.registerSingleton(UI.class,uis); pc.requestMessages(SystemStateMsg.class,this); pc.requestMessages(ErrorMsg.class,this, Msg.PRIORITY_LAST+1); } @Override public void close() throws GeneralException { UISingleton uis=(UISingleton)context.getSingle(UI.class); uis.close(); } /** * Action: agree to license. * @throws GeneralException */ @UIAction public void actionLicenseAgree() throws GeneralException { Preferences prefs=context.getSingle(Preferences.class); PreferencesGroup group=prefs.getGroup(this); group.set(PREF_AGREEDLICENSE,"y"); license.close(); } /** * Action: disagree to license. */ @UIAction public void actionLicenseDisagree() { // Abrupt exit System.exit(0); } /** * Message: system date. Displays license if required, then dispatches the * 'UI ready' system state message. * @param m * @throws GeneralException */ public void msg(SystemStateMsg m) throws GeneralException { if(m.getType() == SystemStateMsg.PLUGINSLOADED) { UISingleton.checkSwing(); // Create window UISingleton uis = (UISingleton)context.getSingle(UI.class); uis.init("leafChat " + SystemVersion.getTitleBarVersion()); // Check license if needed Preferences prefs = context.getSingle(Preferences.class); PreferencesGroup group = prefs.getGroup(this); if(group.get(PREF_AGREEDLICENSE, null) == null) { // Send license display state message m = new SystemStateMsg(SystemStateMsg.LICENSEDIALOG); context.dispatchExternalMessage(SystemStateMsg.class, m, true); // Display license try { license = uis.createDialog(XML.parse( UIPlugin.class.getResourceAsStream("license.xml")), this); licenseTextUI.setStyleSheet("output { pad-left:4; pad-right:4; }"); licenseTextUI.addXML( "<head>GNU General Public License version 3</head>" + "<para>leafChat is free software: you can redistribute it and/or " + "modify it under the terms of the GNU General Public License as " + "published by the Free Software Foundation, either version 3 of " + "the License, or (at your option) any later version.</para>" + "<para>leafChat is distributed in the hope that it will be useful, " + "but WITHOUT ANY WARRANTY; without even the implied warranty of " + "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the " + "GNU General Public License for more details.</para>"+ "<para>To read the license, see " + "<url>http://www.gnu.org/licenses/</url>.</para>" + "<para>leafChat is copyright © 2012 Samuel Marshall.</para>"+ "<head>Other components</head>"+ "<para>The leafChat distribution incorporates external libraries " + "which have their own licenses and copyrights. These can be found " + "in the 'lib' folder "+ "within the distribution. Specifically:</para>"+ "<para>This product includes a modified version of the Eclipse " + "JDT Core compiler package, developed by Eclipse contributors " + "and others.</para>" + "<para>This product includes software developed by SuperBonBon " + "Industries (http://www.sbbi.net/).</para>" + "<para>This product includes JOrbis audio playback library " + "(www.jcraft.com/jorbis/).</para>" ); license.show(null); } catch(XMLException e) { throw new GeneralException("Error parsing license dialog",e); } } // Send message indicating that UI is ready now m = new SystemStateMsg(SystemStateMsg.UIREADY); context.dispatchExternalMessage(SystemStateMsg.class, m, false); } } /** * Handles ErrorMsg to display a dialog box and report error to server. * @param msg Message * @throws GeneralException */ public void msg(ErrorMsg msg) throws GeneralException { if(msg.isHandled()) { return; } if(errorDialog==null) { // Only show the first error in dialog... UI ui = context.getSingle(UI.class); errorDialog = ui.createDialog("errordialog", this); reportUI.setChecked(!SystemVersion.getBuildVersion().startsWith("@")); errorMessageUI.setStyleSheet( "exception { type:block; font-size:0.8f; font-name:'Andale Mono',Monaco,'Courier New',Monospaced; }" + "line { gap-left:4; text-indent:20; text-first-indent:-20; }" + "exception { gap-left:4; gap-top:0.5f; }" + "exception > line { gap-left:0; }" ); errorMessageUI.addXML( "<line><strong>" + XML.esc(msg.getMessage()) + "</strong></line>"); StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); if(msg.getException() != null) { msg.getException().printStackTrace(pw); } pw.flush(); String currentException = sw.toString(), currentMessage = msg.getMessage(); Matcher m = Pattern.compile( "^.*?UserScript(.*?)[$.(].*",Pattern.DOTALL).matcher(currentException); boolean special = false; if(m.matches()) { reportInfoUI.setText("<para>The error was caused by a user script, <key>" + XML.esc(m.group(1))+"</key>, and will not be reported automatically. If " + "you think the error is a system bug and not your script's fault, " + "click the checkbox below to send it in.</para>"); reportUI.setChecked(false); currentException = "SPECIAL_USER_SCRIPT_ERROR\n" + currentException; special = true; } m = Pattern.compile( "^.*the theme (.*?)\\.leafChatTheme.*$",Pattern.DOTALL).matcher(currentMessage); if(!special && m.matches()) { reportInfoUI.setText("<para>The error was caused by a user theme, <key>"+ XML.esc(m.group(1))+"</key>.leafChatTheme, and will not be reported " + "automatically. If you think the error is a system bug and not your theme's fault, " + "click the checkbox below to send it in.</para>"); reportUI.setChecked(false); currentException = "SPECIAL_USER_THEME_ERROR\n" + currentException; special = true; } if(!special && currentException.contains("java.lang.OutOfMemoryError")) { reportInfoUI.setText("<para>The error was caused because leafChat ran " + "out of memory (Java heap space).</para>"); currentException = "SPECIAL_OUT_OF_MEMORY\n" + currentException; special = true; } long freeSpace = IOUtils.getFreeSpace(PlatformUtils.getUserFolder()); if(!special && (currentException.indexOf("not enough space on the disk")!=-1 || currentException.indexOf("No space left on device")!=-1) || (freeSpace != IOUtils.UNKNOWN && freeSpace < 65536L) ) { reportInfoUI.setText("<para><strong>You have no remaining disk " + "space</strong>. To recover, free up space first, then quit " + "leafChat and restart it.</para>"); reportUI.setChecked(false); reportUI.setEnabled(false); special = true; } reporter = new ErrorReportThread(currentMessage, currentException); String trace = currentException; StringBuffer out = new StringBuffer("<exception><line>"); while(true) { int line = trace.indexOf('\n'); if(line == -1) { out.append(XML.esc(trace)); break; } String thisLine = trace.substring(0,line).replaceFirst("^\\s*at\\s+","-"); out.append(XML.esc(thisLine)); out.append("</line><line>"); trace = trace.substring(line+1); } out.append("</line></exception>"); errorMessageUI.addXML(out.toString()); String versions="<para>leafChat <key>" + XML.esc(SystemVersion.getBuildVersion()) + "</key>, Java <key>" + XML.esc(System.getProperty("java.version")) + "</key>, " + XML.esc(System.getProperty("os.name")) + " <key>" + XML.esc(System.getProperty("os.version")) + "</key></para>"; errorMessageUI.addXML(versions); errorDialog.show((Window)null); } msg.markHandled(); (context.getSingle(SystemLog.class)).log( this, "Error handler: " + msg.getMessage(), msg.getException()); } /** * User closes error dialog. */ @UIAction public void actionCloseErrorDialog() { if(reporter!=null && reportUI.isChecked()) { reporter.start(); reporter = null; } // Note: I don't see how it is possible that errorDialog would be null // at this point, but it occurred to one user, so I put the if in. if(errorDialog != null) { errorDialog.close(); } } private class ErrorReportThread extends Thread { private String message,exception; public ErrorReportThread(String message,String exception) { super("Error report thread"); this.message=message; this.exception=exception; } @Override public void run() { try { URL u=new URL("http://live.leafdigital.com/leafchat-remote/report.jsp"); HttpURLConnection uc=(HttpURLConnection)u.openConnection(); uc.setDoOutput(true); uc.setRequestProperty("Content-Type","application/x-www-form-urlencoded"); String output= "lc_version="+URLEncoder.encode(SystemVersion.getBuildVersion(),"UTF-8")+ "&java_version="+URLEncoder.encode(System.getProperty("java.version"),"UTF-8")+ "&os_name="+URLEncoder.encode(System.getProperty("os.name"),"UTF-8")+ "&os_version="+URLEncoder.encode(System.getProperty("os.version"),"UTF-8")+ "&message="+URLEncoder.encode(message,"UTF-8")+ "&exception="+URLEncoder.encode(exception,"UTF-8"); uc.getOutputStream().write(output.getBytes("UTF-8")); uc.getOutputStream().close(); int code=uc.getResponseCode(); if(code!=200) throw new Exception("Unexpected response code "+code); context.log("Error report: successful"); } catch(Throwable t) { context.log("Error report: failed - "+t.getMessage()); } } } /** Error dialog has been closed. */ @UIAction public void closedErrorDialog() { errorDialog = null; } @Override public String toString() { return "UI plugin"; } }