1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 as published by
5    * the Free Software Foundation. This program is distributed in the hope
6    * that it will be useful, but WITHOUT ANY WARRANTY; without even the
7    * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *       http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * Copyright: 2005
18   *     The copyright to this program is held by it's authors.
19   *
20   * ID: $Id: ExceptionPane.java 2223 2012-01-26 21:28:02Z dmsmith $
21   */
22  package org.crosswire.common.swing;
23  
24  import java.awt.BorderLayout;
25  import java.awt.Component;
26  import java.awt.Cursor;
27  import java.awt.Dimension;
28  import java.awt.FlowLayout;
29  import java.awt.Font;
30  import java.awt.Frame;
31  import java.awt.event.ActionEvent;
32  import java.awt.event.ActionListener;
33  import java.awt.event.ItemEvent;
34  import java.awt.event.ItemListener;
35  import java.io.File;
36  import java.io.FileReader;
37  import java.io.IOException;
38  import java.io.LineNumberReader;
39  import java.text.MessageFormat;
40  import java.util.ArrayList;
41  import java.util.List;
42  
43  import javax.swing.BorderFactory;
44  import javax.swing.DefaultComboBoxModel;
45  import javax.swing.JButton;
46  import javax.swing.JCheckBox;
47  import javax.swing.JComboBox;
48  import javax.swing.JDialog;
49  import javax.swing.JLabel;
50  import javax.swing.JList;
51  import javax.swing.JPanel;
52  import javax.swing.JScrollPane;
53  import javax.swing.JSplitPane;
54  import javax.swing.JTextArea;
55  import javax.swing.ListSelectionModel;
56  import javax.swing.SwingUtilities;
57  import javax.swing.event.ListSelectionEvent;
58  import javax.swing.event.ListSelectionListener;
59  
60  import org.crosswire.common.util.FileUtil;
61  import org.crosswire.common.util.Reporter;
62  import org.crosswire.common.util.ReporterEvent;
63  import org.crosswire.common.util.ReporterListener;
64  import org.crosswire.common.util.StackTrace;
65  import org.crosswire.common.xml.XMLUtil;
66  
67  /**
68   * A simple way of reporting problems to the user.
69   * 
70   * @see gnu.lgpl.License for license details.<br>
71   *      The copyright to this program is held by it's authors.
72   * @author Joe Walker [joe at eireneh dot com]
73   */
74  public final class ExceptionPane extends JPanel {
75      /**
76       * Use showExceptionDialog for the time being
77       */
78      private ExceptionPane(Throwable ex) {
79          this.ex = ex;
80          initialise();
81          setDisplayedException(ex);
82      }
83  
84      /**
85       * Setup the GUI
86       */
87      private void initialise() {
88          String exmsg = MessageFormat.format("<html><font size=\"-1\">{0}</font> {1}",
89                  // TRANSLATOR: When an error dialog is presented to the user, this labels the error.
90                  CWMsg.gettext("An error has occurred:"), ExceptionPane.getHTMLDescription(ex)
91          );
92  
93          // The upper pane
94          JLabel message = new JLabel();
95          message.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
96          message.setText(exmsg);
97          message.setIcon(GuiUtil.getIcon("toolbarButtonGraphics/general/Stop24.gif"));
98          message.setIconTextGap(20);
99  
100         JPanel banner = new JPanel(new BorderLayout());
101         banner.add(message, BorderLayout.CENTER);
102         list = new JList();
103         list.setVisibleRowCount(6);
104         list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
105         Font courier = new Font("Monospaced", Font.PLAIN, 12);
106         list.setFont(courier);
107 
108         JPanel buttons = new JPanel(new BorderLayout());
109 
110         okBox = new JPanel(new FlowLayout());
111         buttons.add(okBox, BorderLayout.CENTER);
112 
113         // Add a button if showDetails is true
114         detail = new JCheckBox();
115         detail.addItemListener(new SelectedItemListener(this));
116         // TRANSLATOR: When an error dialog is presented to the user, this labels the details of the error.
117         detail.setText(CWMsg.gettext("Details"));
118         if (detailShown) {
119             buttons.add(detail, BorderLayout.LINE_START);
120         }
121 
122         upper = new JPanel(new BorderLayout());
123         upper.add(banner, BorderLayout.NORTH);
124         upper.add(buttons, BorderLayout.CENTER);
125 
126         List<Throwable> causes = new ArrayList<Throwable>();
127         Throwable throwable = ex;
128         while (throwable != null) {
129             causes.add(throwable);
130             throwable = throwable.getCause();
131         }
132         Throwable[] exs = causes.toArray(new Throwable[causes.size()]);
133 
134         JComboBox traces = new JComboBox();
135         traces.setModel(new DefaultComboBoxModel(exs));
136         traces.addActionListener(new SelectActionListener(this, traces));
137 
138         JPanel heading = new JPanel(new BorderLayout());
139         heading.add(traces, BorderLayout.CENTER);
140 
141         lower = new JPanel(new BorderLayout());
142         lower.add(heading, BorderLayout.NORTH);
143 
144         // If we have sources then show an area for the source.
145         // Otherwise just list the exception trace
146         if (sources.length == 0) {
147             lower.add(new CWScrollPane(list), BorderLayout.CENTER);
148         } else {
149             // The lower pane
150             label = new JLabel();
151             label.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
152             label.setFont(courier);
153             // TRANSLATOR: When an error dialog is presented to the user, this indicates that the Java source is unavailable.
154             label.setText(CWMsg.gettext("No File"));
155             text = new JTextArea();
156             text.setEditable(false);
157             text.setFont(courier);
158 
159             JScrollPane textScroll = new CWScrollPane(text);
160             textScroll.setColumnHeaderView(label);
161 
162             JSplitPane split = new FixedSplitPane();
163             // Make the top 20% of the total
164             split.setResizeWeight(0.2D);
165             split.setOrientation(JSplitPane.VERTICAL_SPLIT);
166             split.setContinuousLayout(true);
167             split.setTopComponent(new CWScrollPane(list));
168             split.setBottomComponent(textScroll);
169             split.setBorder(BorderFactory.createEmptyBorder());
170             split.setPreferredSize(new Dimension(500, 300));
171             lower.add(split, BorderLayout.CENTER);
172         }
173 
174         this.setLayout(new BorderLayout());
175         this.add(upper, BorderLayout.NORTH);
176     }
177 
178     /**
179      * Is the detail area shown?
180      */
181     protected synchronized void changeDetail() {
182         if (detail.isSelected()) {
183             ExceptionPane.this.add(lower, BorderLayout.CENTER);
184         } else {
185             ExceptionPane.this.remove(lower);
186         }
187 
188         GuiUtil.getDialog(ExceptionPane.this).pack();
189     }
190 
191     /**
192      * Display a different nested exception
193      */
194     protected synchronized void setDisplayedException(Throwable ex) {
195         StackTrace st = new StackTrace(ex);
196         if (sources.length > 0) {
197             list.addListSelectionListener(new ExceptionPane.CustomLister(st, text, label));
198         }
199         list.setModel(new StackTraceListModel(st));
200     }
201 
202     /**
203      * Show a dialog containing the exception
204      * 
205      * @param parent
206      *            Something to attach the Dialog to
207      * @param ex
208      *            The Exception to display
209      */
210     public static void showExceptionDialog(Component parent, Throwable ex) {
211         final ExceptionPane pane = new ExceptionPane(ex);
212 
213         // Setting for the whole dialog
214         Frame root = GuiUtil.getFrame(parent);
215 
216         // TRANSLATOR: When an error dialog is presented to the user, this is the title of the dialog.
217         String error = CWMsg.gettext("Error");
218 
219         // If this dialog is not modal then if we display an exception dialog
220         // where there is a modal dialog displayed then although this dialog
221         // is to the front, we can't interact with it until the modal dialog
222         // has been closed.
223         final JDialog dialog = new JDialog(root, error, true);
224         dialog.getRootPane().setLayout(new BorderLayout());
225         dialog.getRootPane().setBorder(BorderFactory.createMatteBorder(5, 5, 5, 5, pane.upper.getBackground()));
226         dialog.getRootPane().add(pane, BorderLayout.CENTER);
227 
228         final ActionFactory actions = new ActionFactory(pane);
229 
230         // TRANSLATOR: This is the text on an "OK" button.
231         JButton ok = actions.createJButton(actions.addAction("OK", CWMsg.gettext("OK")), new ActionListener() {
232             public void actionPerformed(ActionEvent e) {
233                 dialog.dispose();
234             }
235         });
236 
237         pane.okBox.add(ok);
238         dialog.getRootPane().setDefaultButton(ok);
239 
240         GuiUtil.centerOnScreen(dialog);
241         GuiUtil.applyDefaultOrientation(dialog);
242         dialog.pack();
243         dialog.setVisible(true);
244     }
245 
246     /**
247      * This is only used by config
248      * 
249      * @return Whether the "details" check box should be shown.
250      * @see #setDetailShown(boolean)
251      */
252     public static boolean isDetailShown() {
253         return ExceptionPane.detailShown;
254     }
255 
256     /**
257      * Set whether the "details" check box should be shown.
258      * 
259      * @param detailShown
260      *            indicates the whether details should be available.
261      * @see #isDetailShown()
262      */
263     public static void setDetailShown(boolean detailShown) {
264         ExceptionPane.detailShown = detailShown;
265     }
266 
267     /**
268      * Set the directories to search for source files.
269      * 
270      * @param sourcePath
271      *            A string array of the source directories
272      */
273     public static void setSourcePath(File[] sourcePath) {
274         ExceptionPane.sources = sourcePath.clone();
275     }
276 
277     /**
278      * Get the directories searched for source files.
279      * 
280      * @return A string array of the source directories
281      */
282     public static File[] getSourcePath() {
283         return sources.clone();
284     }
285 
286     /**
287      * You must call setJoinHelpDesk() in order to start displaying Exceptions
288      * sent to the Log, and in order to properly close this class you must call
289      * it again (with false).
290      * 
291      * @param joined
292      *            Are we listening to the Log
293      */
294     public static synchronized void setHelpDeskListener(boolean joined) {
295         if (joined) {
296             Reporter.addReporterListener(li);
297         } else {
298             Reporter.removeReporterListener(li);
299         }
300     }
301 
302     /**
303      * Gets a short HTML description of an Exception for display in a window
304      */
305     public static String getHTMLDescription(Throwable ex) {
306         StringBuilder retcode = new StringBuilder();
307 
308         // The message in the exception
309         String msg = ex.getMessage();
310         if (msg == null || "".equals(msg)) {
311             // TRANSLATOR: When an error dialog is presented to the user, this is shown when
312             // there is no other message available
313             msg = CWMsg.gettext("No description available.");
314         }
315         String orig = XMLUtil.escape(msg);
316         msg = orig.replaceAll("\n", "<br>");
317 
318         retcode.append("<br>");
319         retcode.append(msg);
320 
321         // If this is a LucidException with a nested Exception
322         Throwable nex = ex.getCause();
323         if (nex != null) {
324             retcode.append("<p><br><font size=\"-1\">");
325             // TRANSLATOR: When an error dialog is presented to the user, this labels the cause of the error.
326             retcode.append(CWMsg.gettext("This was caused by:"));
327             retcode.append("</font>");
328             retcode.append(getHTMLDescription(nex));
329         }
330 
331         return retcode.toString();
332     }
333 
334     /**
335      *
336      */
337     private static final class SelectedItemListener implements ItemListener {
338         /**
339          * @param ep
340          */
341         public SelectedItemListener(ExceptionPane ep) {
342             pane = ep;
343         }
344 
345         /* (non-Javadoc)
346          * @see java.awt.event.ItemListener#itemStateChanged(java.awt.event.ItemEvent)
347          */
348         public void itemStateChanged(ItemEvent ev) {
349             pane.changeDetail();
350         }
351 
352         private ExceptionPane pane;
353     }
354 
355     /**
356      *
357      */
358     private static final class SelectActionListener implements ActionListener {
359         /**
360          * @param ep
361          * @param cb
362          */
363         public SelectActionListener(ExceptionPane ep, JComboBox cb) {
364             pane = ep;
365             traces = cb;
366         }
367 
368         /* (non-Javadoc)
369          * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
370          */
371         public void actionPerformed(ActionEvent ev) {
372             Throwable th = (Throwable) traces.getSelectedItem();
373             pane.setDisplayedException(th);
374         }
375 
376         private ExceptionPane pane;
377         private JComboBox traces;
378     }
379 
380     /**
381      * List listener to update the contents of the text area whenever someone
382      * clicks in the list
383      */
384     private static final class CustomLister implements ListSelectionListener {
385         /**
386          * Initialize with the stuff we need to act on the change, when the list
387          * is clicked.
388          * 
389          * @param st
390          *            The list of elements in the exception
391          * @param text
392          *            The editable file
393          * @param label
394          *            The filename label
395          */
396         public CustomLister(StackTrace st, JTextArea text, JLabel label) {
397             this.st = st;
398             this.mytext = text;
399             this.mylabel = label;
400         }
401 
402         /* (non-Javadoc)
403          * @see javax.swing.event.ListSelectionListener#valueChanged(javax.swing.event.ListSelectionEvent)
404          */
405         public void valueChanged(ListSelectionEvent ev) {
406             if (ev.getValueIsAdjusting()) {
407                 return;
408             }
409 
410             // Wait cursor
411             SwingUtilities.getRoot(mylabel).setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
412 
413             // Get a stack trace
414             JList lst = (JList) ev.getSource();
415             int level = lst.getSelectedIndex();
416             String name = st.getClassName(level);
417 
418             if (name.indexOf('$') != -1) {
419                 name = name.substring(0, name.indexOf('$'));
420             }
421 
422             int line_num = st.getLineNumber(level);
423             String orig = name;
424             Integer errorLine = Integer.valueOf(line_num);
425             // TRANSLATOR: When an error dialog is presented to the user, this indicates that the Java source is unavailable.
426             mylabel.setText(CWMsg.gettext("No File"));
427 
428             // Find a file
429             name = File.separator + orig.replace('.', File.separatorChar) + FileUtil.EXTENSION_JAVA;
430             File[] srcs = ExceptionPane.getSourcePath();
431             for (int i = 0; i < srcs.length; i++) {
432                 File file = new File(srcs[i], name);
433                 if (file.isFile() && file.canRead()) {
434                     // Found the file, load it into the window
435                     StringBuilder data = new StringBuilder();
436 
437                     // Attempt to note the line to highlight
438                     int selection_start = 0;
439                     int selection_end = 0;
440 
441                     LineNumberReader in = null;
442                     try {
443                         // TRANSLATOR: When an error dialog is presented to the user, this indicates that the location of the error in the Java source.
444                         // {0} is a placeholder for the line number on which the error occurred.
445                         // {1} is a placeholder for the Java file.
446                         String found = CWMsg.gettext("Error on line {0} in file {1}", errorLine, file.getCanonicalPath());
447                         mylabel.setText(found);
448                         in = new LineNumberReader(new FileReader(file));
449                         while (true) {
450                             String line = in.readLine();
451                             if (line == null) {
452                                 break;
453                             }
454                             data.append(line).append('\n');
455 
456                             int current_line = in.getLineNumber();
457                             if (current_line == line_num - 1) {
458                                 selection_start = data.length();
459                             }
460                             if (current_line == line_num) {
461                                 selection_end = data.length() - 1;
462                             }
463                         }
464                     } catch (IOException ex) {
465                         data.append(ex.getMessage());
466                     } finally {
467                         if (in != null) {
468                             try {
469                                 in.close();
470                             } catch (IOException e) {
471                                 data.append(e.getMessage());
472                             }
473                         }
474                     }
475 
476                     // Actually set the text
477                     mytext.setText(data.toString());
478                     mytext.setSelectionStart(selection_start);
479                     mytext.setSelectionEnd(selection_end);
480 
481                     SwingUtilities.getRoot(mylabel).setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
482                     return;
483                 }
484             }
485 
486             // TRANSLATOR: When an error dialog is presented to the user, this indicates that the Java source could not be found.
487             // {1} is a placeholder for the line number on which the error occurred.
488             // {0} is a placeholder for the Java file.
489             StringBuilder error = new StringBuilder(CWMsg.gettext("Cannot open source for: {0}, line: {1}\n", st.getClassName(level), errorLine));
490             for (int i = 0; i < srcs.length; i++) {
491                 // TRANSLATOR: When an error dialog is presented to the user, and the Java source could not be found
492                 // this indicates what locations were tried.
493                 // {0} is a placeholder for the location.
494                 error.append(CWMsg.gettext("Tried: {0}\n", srcs[i].getAbsolutePath() + name));
495             }
496 
497             mytext.setText(error.toString());
498             SwingUtilities.getRoot(mylabel).setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
499         }
500 
501         /**
502          * The StackTrace
503          */
504         private StackTrace st;
505 
506         /**
507          * The Text to write to
508          */
509         private JTextArea mytext;
510 
511         /**
512          * The Text to write to
513          */
514         private JLabel mylabel;
515     }
516 
517     /**
518      * The ExceptionPane instance that we add to the Log
519      */
520     static final class ExceptionPaneReporterListener implements ReporterListener {
521         /**
522          * Called whenever Reporter.informUser() is passed an Exception
523          * 
524          * @param ev
525          *            The event describing the Exception
526          */
527         public void reportException(ReporterEvent ev) {
528             // This is to ensure that we don't break any SwingThread rules
529             SwingUtilities.invokeLater(new ExceptionRunner(ev));
530         }
531 
532         /**
533          * Called whenever Reporter.informUser() is passed a message
534          * 
535          * @param ev
536          *            The event describing the message
537          */
538         public void reportMessage(ReporterEvent ev) {
539             // This is to ensure that we don't break any SwingThread rules
540             SwingUtilities.invokeLater(new MessageRunner(ev));
541         }
542     }
543 
544     /**
545     *
546     */
547     private static final class ExceptionRunner implements Runnable {
548         /**
549          * @param ev
550          */
551         public ExceptionRunner(ReporterEvent ev) {
552             event = ev;
553         }
554 
555         /*
556          * (non-Javadoc)
557          * 
558          * @see java.lang.Runnable#run()
559          */
560         public void run() {
561             if (event.getSource() instanceof Component) {
562                 showExceptionDialog((Component) event.getSource(), event.getException());
563             } else {
564                 showExceptionDialog(null, event.getException());
565             }
566         }
567 
568         private ReporterEvent event;
569     }
570 
571     /**
572      *
573      */
574     private static final class MessageRunner implements Runnable {
575         /**
576          * @param ev
577          */
578         public MessageRunner(ReporterEvent ev) {
579             event = ev;
580         }
581 
582         /* (non-Javadoc)
583          * @see java.lang.Runnable#run()
584          */
585         public void run() {
586             if (event.getSource() instanceof Component) {
587                 CWOptionPane.showMessageDialog((Component) event.getSource(), event.getMessage());
588             } else {
589                 CWOptionPane.showMessageDialog(null, event.getMessage());
590             }
591         }
592 
593         private ReporterEvent event;
594     }
595 
596     /**
597      * The exception we are displaying
598      */
599     private Throwable ex;
600 
601     // The components - contained, top to containing, bottom
602     private JList list;
603     private JPanel upper;
604     private JLabel label;
605     private JTextArea text;
606     private JPanel okBox;
607     private JCheckBox detail;
608     private JPanel lower;
609 
610     /**
611      * Whether full details should be given.
612      */
613     private static boolean detailShown;
614 
615     /**
616      * The directories searched for source
617      */
618     private static File[] sources = new File[0];
619 
620     /**
621      * The listener that pops up the ExceptionPanes
622      */
623     private static ExceptionPaneReporterListener li = new ExceptionPaneReporterListener();
624 
625     /**
626      * Serialization ID
627      */
628     private static final long serialVersionUID = 3258126947203495219L;
629 }
630