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("<", "&lt;")
162                   .replaceAll(">", "&gt;")
163                   .replaceAll("\"", "\\\"")
164                   .replace("\n", "<br>")
165                   .replace(" ", "&nbsp;")
166                   .replace("\t", "&nbsp;&nbsp;&nbsp;");
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    }