1 | // Jomic - a viewer for comic book archives. |
2 | // Copyright (C) 2004-2011 Thomas Aglassinger |
3 | // |
4 | // This program is free software: you can redistribute it and/or modify |
5 | // it under the terms of the GNU General Public License as published by |
6 | // the Free Software Foundation, either version 3 of the License, or |
7 | // (at your option) any later version. |
8 | // |
9 | // This program is distributed in the hope that it will be useful, |
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | // GNU General Public License for more details. |
13 | // |
14 | // You should have received a copy of the GNU General Public License |
15 | // along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | package net.sf.jomic.tools; |
17 | |
18 | import java.awt.Component; |
19 | import java.awt.Dimension; |
20 | import java.awt.event.ActionEvent; |
21 | import java.awt.event.ItemEvent; |
22 | import java.awt.event.ItemListener; |
23 | import java.beans.PropertyChangeEvent; |
24 | import java.beans.PropertyChangeListener; |
25 | import java.io.PrintStream; |
26 | import java.io.PrintWriter; |
27 | import java.io.StringWriter; |
28 | import java.text.MessageFormat; |
29 | import java.util.LinkedList; |
30 | import java.util.List; |
31 | import java.util.Map; |
32 | import java.util.TreeMap; |
33 | |
34 | import javax.swing.JCheckBox; |
35 | import javax.swing.JDialog; |
36 | import javax.swing.JFrame; |
37 | import javax.swing.JOptionPane; |
38 | import javax.swing.JScrollPane; |
39 | import javax.swing.JTextArea; |
40 | import javax.swing.WindowConstants; |
41 | |
42 | import net.sf.jomic.common.PropertyConstants; |
43 | |
44 | /** |
45 | * Tools for error handling and reporting. This class is self contained and does not require any |
46 | * other classes from the tools package, therefor its methods can be used to handle errors right |
47 | * from the beginning. |
48 | * |
49 | * @author Thomas Aglassinger |
50 | */ |
51 | public final class ErrorTools |
52 | { |
53 | private static final int UPPER_CENTER_HEIGHT_DIVISOR = 4; |
54 | private static /*@ spec_public nullable @*/ ErrorTools instance; |
55 | |
56 | private /*@ spec_public @*/ String cannotChangePropertyText; |
57 | private /*@ spec_public @*/ String cannotProcessActionCommand; |
58 | private /*@ spec_public @*/ String cannotProcessActionEvent; |
59 | private int messageCount; |
60 | private /*@ spec_public @*/ String showStackTraceText; |
61 | private Map typeToTitleMap; |
62 | |
63 | private ErrorTools() { |
64 | typeToTitleMap = new TreeMap(); |
65 | showStackTraceText = "Show internal call stack"; |
66 | cannotChangePropertyText = "cannot change property {0} from \"{1}\" to \"{2}\""; |
67 | cannotProcessActionCommand = "cannot process internal command \"{0}\""; |
68 | cannotProcessActionEvent = "cannot process ActionEvent: {0}"; |
69 | setTitle(JOptionPane.WARNING_MESSAGE, "Warning"); |
70 | setTitle(JOptionPane.ERROR_MESSAGE, "Error"); |
71 | } |
72 | |
73 | public void setCannotChangePropertyText(String newCannotChangePropertyText) { |
74 | cannotChangePropertyText = newCannotChangePropertyText; |
75 | } |
76 | |
77 | public void setCannotProcessActionCommand(String newCannotProcessActionCommand) { |
78 | cannotProcessActionCommand = newCannotProcessActionCommand; |
79 | } |
80 | |
81 | public void setCannotProcessActionEvent(String newCannotProcessActionEvent) { |
82 | cannotProcessActionEvent = newCannotProcessActionEvent; |
83 | } |
84 | |
85 | /** |
86 | * Set text for "Show stack trace button" in dialogs. |
87 | * |
88 | * @see #createDialog(JFrame, int, String, Throwable, boolean) |
89 | */ |
90 | //@ assignable showStackTraceText; |
91 | public void setShowStackTraceText(String newText) { |
92 | showStackTraceText = newText; |
93 | } |
94 | |
95 | /** |
96 | * Set title for dialog of a certain type. |
97 | * |
98 | * @see JOptionPane |
99 | * @param type one of <code>JOptionPane.*_MESSAGE</code> |
100 | */ |
101 | public void setTitle(int type, String title) { |
102 | assertValidMessageType(type); |
103 | typeToTitleMap.put(new Integer(type), title); |
104 | } |
105 | |
106 | /** |
107 | * Get detailed exception message by concatenating <code>getMessage()</code> of <code>error</code> |
108 | * and all nested exceptions. |
109 | * |
110 | * @see Throwable#getCause() |
111 | */ |
112 | public String getDetailedExceptionMessage(Throwable error) { |
113 | String result = ""; |
114 | Throwable cause = error; |
115 | |
116 | while (cause != null) { |
117 | String message = cause.getMessage(); |
118 | Class clazz = cause.getClass(); |
119 | String clazzName = clazz.getName(); |
120 | boolean includeClassName = (message == null); |
121 | |
122 | if ((cause instanceof NullPointerException) || (cause instanceof AssertionError) |
123 | || (cause instanceof ClassCastException)) { |
124 | includeClassName = true; |
125 | } |
126 | if (message == null) { |
127 | message = ""; |
128 | } else if (includeClassName) { |
129 | message = ": " + message; |
130 | } |
131 | |
132 | if (includeClassName) { |
133 | int lastDotIndex = clazzName.lastIndexOf('.'); |
134 | |
135 | if (lastDotIndex >= 0) { |
136 | clazzName = clazzName.substring(lastDotIndex + 1); |
137 | } |
138 | message = clazzName + message; |
139 | } |
140 | if (result.length() > 0) { |
141 | result += ":\n"; |
142 | } |
143 | result += titled(message); |
144 | cause = cause.getCause(); |
145 | } |
146 | return result; |
147 | } |
148 | |
149 | /** |
150 | * Get number of message dialogs shown so far. |
151 | */ |
152 | //@ ensures \result >= 0; |
153 | public int getMessageCount() { |
154 | return messageCount; |
155 | } |
156 | |
157 | /** |
158 | * Stack trace as string. |
159 | */ |
160 | public String getStackTrace(Throwable error) { |
161 | String result; |
162 | StringWriter stringWriter = new StringWriter(); |
163 | PrintWriter printWriter = new PrintWriter(stringWriter); |
164 | |
165 | error.printStackTrace(printWriter); |
166 | result = stringWriter.getBuffer().toString(); |
167 | return result; |
168 | } |
169 | |
170 | public String getTitle(int type) { |
171 | String result = (String) typeToTitleMap.get(new Integer(type)); |
172 | |
173 | return result; |
174 | } |
175 | |
176 | //@ ensures instance != null; |
177 | public static synchronized ErrorTools instance() { |
178 | if (instance == null) { |
179 | instance = new ErrorTools(); |
180 | } |
181 | return instance; |
182 | } |
183 | |
184 | /** |
185 | * Center frame in upper half of the screen. |
186 | */ |
187 | public void centerUp(Component frame) { |
188 | center(frame, UPPER_CENTER_HEIGHT_DIVISOR); |
189 | } |
190 | |
191 | /** |
192 | * Create error dialog. |
193 | * |
194 | * @param owner Frame to which error belongs, or <code>null</code> |
195 | * @param type one of <code>JOptionPane.*_MESSAGE</code> |
196 | * @param error null or stack that caused error |
197 | */ |
198 | public JDialog createDialog(/*@ nullable @*/ JFrame owner, int type, String message, |
199 | /*@ nullable @*/ Throwable error, boolean modal) { |
200 | assert message != null; |
201 | assertValidMessageType(type); |
202 | |
203 | final JDialog dialog = new JDialog(owner, getTitle(type), modal); |
204 | final ItemListener showStackListener; |
205 | final JCheckBox showStackCheckBox; |
206 | final boolean hasShowStackCheckBox = (error != null); |
207 | String errorMessage; |
208 | |
209 | if (error == null) { |
210 | errorMessage = ""; |
211 | } else { |
212 | errorMessage = ":\n" + getDetailedExceptionMessage(error); |
213 | } |
214 | |
215 | List dialogItems = new LinkedList(); |
216 | |
217 | dialogItems.add(titled(message) + errorMessage); |
218 | if (!hasShowStackCheckBox) { |
219 | showStackListener = null; |
220 | showStackCheckBox = null; |
221 | } else { |
222 | showStackCheckBox = new JCheckBox(showStackTraceText); |
223 | |
224 | String stackTrace = getStackTrace(error).trim(); |
225 | JTextArea stackArea = new JTextArea(stackTrace); |
226 | final JScrollPane stackScrollPane = new JScrollPane(stackArea); |
227 | |
228 | dialogItems.add(showStackCheckBox); |
229 | dialogItems.add(stackScrollPane); |
230 | showStackListener = |
231 | new ItemListener() |
232 | { |
233 | public void itemStateChanged(ItemEvent event) { |
234 | stackScrollPane.setVisible(event.getStateChange() == ItemEvent.SELECTED); |
235 | dialog.pack(); |
236 | } |
237 | }; |
238 | showStackCheckBox.addItemListener(showStackListener); |
239 | stackScrollPane.setVisible(false); |
240 | } |
241 | |
242 | final JOptionPane optionPane = new JOptionPane(dialogItems.toArray(), type); |
243 | PropertyChangeListener propertyListener = |
244 | new PropertyChangeListener() |
245 | { |
246 | public void propertyChange(PropertyChangeEvent event) { |
247 | String propertyName = event.getPropertyName(); |
248 | |
249 | if (dialog.isVisible() |
250 | && (event.getSource() == optionPane) |
251 | && (propertyName.equals(JOptionPane.VALUE_PROPERTY))) { |
252 | if (hasShowStackCheckBox) { |
253 | showStackCheckBox.removeItemListener(showStackListener); |
254 | } |
255 | dialog.setVisible(false); |
256 | } |
257 | } |
258 | }; |
259 | |
260 | optionPane.addPropertyChangeListener(propertyListener); |
261 | dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); |
262 | dialog.setContentPane(optionPane); |
263 | dialog.pack(); |
264 | centerUp(dialog); |
265 | return dialog; |
266 | } |
267 | |
268 | /** |
269 | * Show error message for being unable to change property according to <code>event</code>. |
270 | */ |
271 | public void showError(PropertyChangeEvent event, Throwable error) { |
272 | String message = MessageFormat.format(cannotChangePropertyText, |
273 | new Object[]{event.getPropertyName(), event.getOldValue(), event.getNewValue()}); |
274 | |
275 | showErrorMessage(null, message, error); |
276 | } |
277 | |
278 | /** |
279 | * Show error message for being unable to process <code>event</code> (which may be null). |
280 | */ |
281 | public void showError(/*@ nullable @*/ ActionEvent event, Throwable error) { |
282 | String command = null; |
283 | Object messageOption; |
284 | String messagePattern; |
285 | |
286 | if (event != null) { |
287 | command = event.getActionCommand(); |
288 | } |
289 | |
290 | if (command == null) { |
291 | messagePattern = cannotProcessActionEvent; |
292 | messageOption = event; |
293 | } else { |
294 | messagePattern = cannotProcessActionCommand; |
295 | messageOption = command; |
296 | } |
297 | |
298 | String message = MessageFormat.format(messagePattern, new Object[]{messageOption}); |
299 | |
300 | showErrorMessage(null, message, error); |
301 | } |
302 | |
303 | /** |
304 | * Show error dialog. |
305 | * |
306 | * @see #showErrorMessage(JFrame, String, Throwable) |
307 | */ |
308 | public void showErrorMessage(/*@ nullable @*/ JFrame owner, String message, |
309 | /*@ nullable @*/ Throwable error) { |
310 | showMessage(owner, JOptionPane.ERROR_MESSAGE, message, error, true); |
311 | } |
312 | |
313 | /** |
314 | * Show error dialog, optionally allowing user to inspect call stack. |
315 | * |
316 | * @see JOptionPane |
317 | * @see #createDialog(JFrame, int, String, Throwable, boolean) |
318 | * @param owner Frame to which error belongs, or <code>null</code> |
319 | * @param type one of <code>JOptionPane.*_MESSAGE</code> |
320 | * @param error <code>Throwable</code> that caused the error, or null |
321 | * @param modal <code>true</code> to wait for user to click dialog away |
322 | */ |
323 | //@ requires (type == JOptionPane.INFORMATION_MESSAGE) |
324 | //@ || (type == JOptionPane.WARNING_MESSAGE) |
325 | //@ || (type == JOptionPane.ERROR_MESSAGE) |
326 | //@ || (type == JOptionPane.PLAIN_MESSAGE) |
327 | //@ || (type == JOptionPane.QUESTION_MESSAGE); |
328 | public void showMessage(/*@ nullable @*/ JFrame owner, int type, String message, |
329 | /*@ nullable @*/ Throwable error, boolean modal) { |
330 | assertValidMessageType(type); |
331 | |
332 | JDialog dialog = createDialog(owner, type, message, error, modal); |
333 | |
334 | if (Boolean.getBoolean(PropertyConstants.TEST_IGNORE_MESSAGE_DIALOGS)) { |
335 | PrintStream errorStream = System.err; |
336 | |
337 | errorStream.println(message); |
338 | } else { |
339 | dialog.setVisible(true); |
340 | } |
341 | messageCount += 1; |
342 | } |
343 | |
344 | /** |
345 | * Some as <code>some</code>, but with the first character converted to title case. |
346 | * |
347 | * @see Character#toTitleCase(char) |
348 | */ |
349 | public /*@ pure @*/ String titled(String some) { |
350 | String result; |
351 | |
352 | if (some != null) { |
353 | StringBuffer buffer = new StringBuffer(some); |
354 | |
355 | if (some.length() != 0) { |
356 | buffer.setCharAt(0, Character.toTitleCase(some.charAt(0))); |
357 | } |
358 | result = buffer.toString(); |
359 | } else { |
360 | result = "null"; |
361 | } |
362 | return result; |
363 | } |
364 | |
365 | private void assertValidMessageType(int type) { |
366 | assert (type == JOptionPane.ERROR_MESSAGE) || (type == JOptionPane.WARNING_MESSAGE) : "type=" + type; |
367 | } |
368 | |
369 | //@ requires heightDivisor > 0; |
370 | private void center(Component frame, int heightDivisor) { |
371 | Dimension screenSize = frame.getToolkit().getScreenSize(); |
372 | Dimension frameSize = frame.getSize(); |
373 | |
374 | frame.setLocation((screenSize.width - frameSize.width) / 2, |
375 | (screenSize.height - frameSize.height) / heightDivisor); |
376 | } |
377 | } |