001 package fang;
002
003 import javax.swing.*;
004 import javax.swing.text.html.HTMLEditorKit;
005 import javax.swing.text.html.StyleSheet;
006 import java.awt.*;
007 import java.awt.event.*;
008 import java.io.BufferedReader;
009 import java.io.IOException;
010 import java.io.InputStreamReader;
011 import java.io.PrintWriter;
012 import java.io.StringWriter;
013 import java.net.URL;
014 import java.util.LinkedList;
015 import java.util.regex.Pattern;
016
017
018 /**
019 * Displays runtime errors in a meaningful way.
020 * @author Jam Jenkins
021 */
022 public class ErrorConsole extends JDialog implements ActionListener
023 {
024 /**
025 * used for serialization versioning
026 */
027 private static final long serialVersionUID = 1L;
028
029 /**the stylesheet used to display the html*/
030 private static final URL STYLE_SHEET =
031 ErrorConsole.class.getResource("resources/stylesheet.css");
032
033 /** where the html is displayed */
034 private JTextPane message;
035
036 /** default size of the window */
037 private static final Dimension DEFAULT_SIZE = new Dimension(600, 600);
038
039 /** close/next button */
040 private JButton closeButton;
041
042 /** only one error console is needed, this is the only one constructed*/
043 private static final ErrorConsole single = new ErrorConsole();
044
045 /** list of all of errors that have occurred*/
046 private LinkedList<String> errors = new LinkedList<String>();
047
048 /**
049 * makes the error window, but does not set it visible
050 */
051 private ErrorConsole()
052 {
053 super();
054 setTitle("Runtime Errors");
055 makeComponents();
056 makeLayout();
057 setSize(DEFAULT_SIZE);
058 }
059
060 /**advances to the next error and makes this gui invisible
061 * when there are no more errors
062 * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
063 */
064 public void actionPerformed(ActionEvent e)
065 {
066 if (errors.size() == 0)
067 {
068 setVisible(false);
069 }
070 else if (errors.size() == 1)
071 {
072 errors.removeFirst();
073 setVisible(false);
074 }
075 else if (errors.size() > 1)
076 {
077 errors.removeFirst();
078 message.setText(errors.getFirst());
079 message.setCaretPosition(0);
080 if (errors.size() == 1)
081 {
082 closeButton.setText("Close Error Console");
083 }
084 }
085 }
086
087 /** make the message pane and set its contents */
088 private void makeComponents()
089 {
090 message = new JTextPane();
091 closeButton = new FunButton("Close Error Console", DEFAULT_SIZE);
092 closeButton.addActionListener(this);
093 HTMLEditorKit kit = new HTMLEditorKit();
094 StyleSheet style = new StyleSheet();
095 style.importStyleSheet(STYLE_SHEET);
096 kit.setStyleSheet(style);
097 message.setEditorKit(kit);
098 message.setEditable(false);
099 }
100
101 /** place the message pane in the window */
102 private void makeLayout()
103 {
104 Container container = getContentPane();
105 container.setLayout(new BorderLayout());
106 container.add(new JScrollPane(message), BorderLayout.CENTER);
107 container.add(closeButton, BorderLayout.SOUTH);
108 }
109
110 /**
111 * gets the line of the file where the error is.
112 * This reads from the file until a semicolon is found.
113 * @param fileName the name of the file to read from
114 * @param lineNumber the line to return the contents of
115 * @return the statement starting at the given line
116 */
117 public static String getLine(String fileName, int lineNumber)
118 {
119 try
120 {
121 //System.out.println("Error file is " + fileName);
122 String thisClassName = ErrorConsole.class.getCanonicalName();
123 int numPackages = thisClassName.split(Pattern.quote(".")).length - 1;
124 String dirPrefix = "";
125 for (int i = 0; i < numPackages; i++)
126 {
127 dirPrefix += "../";
128 }
129 URL url = ErrorConsole.class.getResource(dirPrefix + fileName);
130 InputStreamReader fromURL = new InputStreamReader(url.openStream());
131 BufferedReader reader = new BufferedReader(fromURL);
132 String line = reader.readLine();
133 int currentLineNumber = 1;
134 while (currentLineNumber != lineNumber)
135 {
136 line = reader.readLine();
137 currentLineNumber++;
138 }
139 while (line.indexOf(";") < 0)
140 {
141 line += "\n" + reader.readLine();
142 }
143 return line;
144 }
145 catch (IOException e)
146 {
147 e.printStackTrace();
148 }
149 return null;
150 }
151
152 /**
153 * replaces the symbols less than, greater than, quotes,
154 * new lines, spaces, and tabs with the corresponding
155 * html to display these properly.
156 * @param text the text to convert, usually Java code
157 * @return the html for displaying the text properly
158 */
159 public static String fixHTML(String text)
160 {
161 return text.replaceAll("<", "<")
162 .replaceAll(">", ">")
163 .replaceAll("\"", "\\\"")
164 .replace("\n", "<br>")
165 .replace(" ", " ")
166 .replace("\t", " ");
167 }
168
169 /**
170 * makes the text monospaced in html
171 * @param text the text to make monospaced, typically file names and code
172 * @return text surrounded with a tag to make it monospaced
173 */
174 public static String fixedWidth(String text)
175 {
176 return "<span style=\"font-family: monospace; font-weight: bold;color: rgb(255, 255, 0);\">" +
177 text + "</span>";
178 }
179
180 /**
181 * indents the given text 40 pixels
182 * @param text the string to indent
183 * @return text surrounded with a tag to indent it 40 pixels
184 */
185 public static String indent(String text)
186 {
187 return "<div style=\"margin-left: 40px;\">" + text + "</div>";
188 }
189
190 /**
191 * makes the text large
192 * @param text the heading
193 * @return text surrounded with a tag for making it large
194 */
195 public static String heading(String text)
196 {
197 return "<h1 style=\"color: rgb(255, 255, 255);\">" + text + "</h1><br>";
198 }
199
200 /**
201 * makes the text slightly smaller than the heading
202 * @param text the subheading
203 * @return text surrounded with a tag for making it large
204 */
205 public static String subHeading(String text)
206 {
207 return "<h2 style=\"color: rgb(255, 255, 255);\">" + text + "</h2>";
208 }
209
210 /**
211 * gets the text for displaying the error's location
212 * @param e the exception that generated the error
213 * @return detailed information about where the error occurred
214 */
215 public static String getLocationSection(Throwable e)
216 {
217 String message =
218 subHeading("Error Location") +
219 "This error was generated by line " +
220 getErrorLineNumber(e) + " of the file<br>" +
221 indent(fixedWidth(getErrorFile(e))) + "<br>" +
222 "This line is <br>" +
223 fixedWidth(fixHTML(getErrorLine(e))) + "<br>";
224 if (e != null)
225 {
226 message += "<br>Exception Stack Trace:<br><br>";
227 StringWriter writer = new StringWriter();
228 e.printStackTrace(new PrintWriter(writer));
229 message += fixedWidth("<pre>" + writer.toString() + "</pre>");
230 }
231 return message;
232 }
233
234 /**
235 * gets the text of the line where the error occurred
236 * @return the line of the error ending in a semicolon
237 */
238 public static String getErrorLine(Throwable t)
239 {
240 return getLine(getErrorFile(t), getErrorLineNumber(t));
241 }
242
243 /**
244 * gets the line number where the error occurred
245 * @return the line number
246 */
247 public static int getErrorLineNumber(Throwable t)
248 {
249 return getErrorElement(t).getLineNumber();
250 }
251
252 /**
253 * gets the name of the method where the error occurred
254 * @return the method name
255 */
256 public static String getErrorMethod(Throwable t)
257 {
258 return getErrorElement(t).getMethodName();
259 }
260
261 /**
262 * gets the name of the source file where the error occurred
263 * @return the file name of the code with the error in it
264 */
265 public static String getErrorFile(Throwable t)
266 {
267 StackTraceElement element = getErrorElement(t);
268 String fileName = element.getClassName();
269 fileName = fileName.replace('.', '/');
270 fileName = fileName + ".java";
271 return fileName;
272 }
273
274 /**
275 * iterates through the execution stack to find the first
276 * element which is outside of the FANG Engine
277 * @return the stack trace element of the code with the error
278 */
279 public static StackTraceElement getErrorElement(Throwable t)
280 {
281 StackTraceElement[] all = t.getStackTrace();
282 int i;
283 for (i = 0; i < all.length; i++)
284 {
285 String packageName = all[i].getClassName();
286 if (!packageName.startsWith("fang") &&
287 !packageName.startsWith("java"))
288 {
289 break;
290 }
291 }
292 if (i == all.length)
293 {
294 return all[i -1];
295 }
296 return all[i];
297 }
298
299 /**sets the content of the window displaying
300 * the error screen. If more than one error
301 * occurs, this adds the error to the queue.
302 * @param title the title of the JDialog box
303 * @param content the content of the help
304 */
305 public static void addError(String diagnosis, String fix, Throwable e)
306 {
307 String content =
308 heading(e.getClass().getCanonicalName()) +
309 subHeading("Diagnosis") +
310 diagnosis +
311 "<br>" + subHeading("Suggested Fix") +
312 fix +
313 getLocationSection(e);
314 single.errors.add(content);
315 if (single.errors.size() == 1)
316 {
317 single.message.setText(content);
318 single.message.setCaretPosition(0);
319 single.setVisible(true);
320 }
321 else
322 {
323 single.closeButton.setText("Next Error Message");
324 }
325 }
326
327 /**
328 * call this method for errors which should not occur.
329 * If they do occur, the FANG Engine developers need to
330 * know about it.
331 * @param e the unexpected exception
332 */
333 public static void addUnknownError(Throwable e)
334 {
335 addError("Strange, this error does not come up often.",
336 "Please make a jar of this game. Email " +
337 "bug@fangengine.org the jar file along" +
338 " with a description of how to recreate" +
339 " the error.", e);
340 }
341
342 /**this catches uncaught exceptions when the game runs
343 * as an application. The primary uncaught exceptions
344 * are initializer errors in games run as applications.
345 * Unfortunately, nothing can detect initializer errors
346 * in applets very well.
347 */
348 public static void registerExceptionHandler()
349 {
350 Thread.currentThread().setUncaughtExceptionHandler(new TopExceptionHandler());
351 }
352
353 /**this class forwards uncaught exceptions to addUnknownError*/
354 private static class TopExceptionHandler
355 implements Thread.UncaughtExceptionHandler
356 {
357 /**forwards uncaught exceptions to addUnknownError*/
358 public void uncaughtException(Thread arg0, Throwable arg1)
359 {
360 addUnknownError(arg1);
361 }
362 }
363 }