001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.core;
043:
044: import java.awt.BorderLayout;
045: import java.awt.Component;
046: import java.awt.Dialog;
047: import java.awt.Dimension;
048: import java.awt.Font;
049: import java.awt.GraphicsEnvironment;
050: import java.awt.Rectangle;
051: import java.awt.Window;
052: import java.awt.event.ActionListener;
053: import java.io.PrintWriter;
054: import java.io.StringWriter;
055: import java.lang.reflect.ParameterizedType;
056: import java.lang.reflect.Type;
057: import java.util.ArrayList;
058: import java.util.ResourceBundle;
059: import java.util.concurrent.Callable;
060: import java.util.logging.Handler;
061: import java.util.logging.Level;
062: import java.util.logging.LogRecord;
063: import java.util.logging.Logger;
064: import javax.swing.Icon;
065: import javax.swing.ImageIcon;
066: import javax.swing.JButton;
067: import javax.swing.JDialog;
068: import javax.swing.JPanel;
069: import javax.swing.JScrollPane;
070: import javax.swing.JTextPane;
071: import javax.swing.SwingUtilities;
072: import javax.swing.UIManager;
073: import org.netbeans.core.startup.CLIOptions;
074: import org.openide.DialogDescriptor;
075: import org.openide.DialogDisplayer;
076: import org.openide.NotifyDescriptor;
077: import org.openide.awt.Mnemonics;
078: import org.openide.util.Exceptions;
079: import org.openide.util.NbBundle;
080: import org.openide.util.Utilities;
081: import org.openide.windows.WindowManager;
082:
083: /**
084: * Notifies exceptions.
085: *
086: * This class is public only because the MainWindow needs get the flashing
087: * icon to its status bar from this class (method getNotificationVisualizer()).
088: *
089: * @author Jaroslav Tulach
090: */
091: public final class NotifyExcPanel extends JPanel implements
092: ActionListener {
093: static final long serialVersionUID = 3680397500573480127L;
094:
095: /** the instance */
096: private static NotifyExcPanel INSTANCE = null;
097: /** preferred width of this component */
098: private static final int SIZE_PREFERRED_WIDTH = 550;
099: /** preferred height of this component */
100: private static final int SIZE_PREFERRED_HEIGHT = 250;
101:
102: /** enumeration of NbExceptionManager.Exc to notify */
103: private static ArrayListPos exceptions;
104: /** current exception */
105: private NbErrorManager.Exc current;
106:
107: /** dialog descriptor */
108: private DialogDescriptor descriptor;
109: /** dialog that displayes the exceptions */
110: java.awt.Dialog dialog;
111: /** button to show next exceptions */
112: private JButton next;
113: /** button to show previous exceptions */
114: private JButton previous;
115: /** details button */
116: private JButton details;
117: /** details window */
118: private JTextPane output;
119:
120: /** boolean to show/hide details */
121: private static boolean showDetails;
122:
123: /** the last position of the exception dialog window */
124: private static Rectangle lastBounds;
125:
126: /** Constructor.
127: */
128: private NotifyExcPanel() {
129: setPreferredSize(new Dimension(SIZE_PREFERRED_WIDTH,
130: SIZE_PREFERRED_HEIGHT));
131:
132: java.util.ResourceBundle bundle = org.openide.util.NbBundle
133: .getBundle(NotifyExcPanel.class);
134: next = new JButton();
135: Mnemonics.setLocalizedText(next, bundle
136: .getString("CTL_NextException"));
137: // bugfix 25684, don't set Previous/Next as default capable
138: next.setDefaultCapable(false);
139: previous = new JButton();
140: Mnemonics.setLocalizedText(previous, bundle
141: .getString("CTL_PreviousException"));
142: previous.setDefaultCapable(false);
143: details = new JButton();
144: details.setDefaultCapable(false);
145:
146: output = new JTextPane() {
147: public boolean getScrollableTracksViewportWidth() {
148: return false;
149: }
150: };
151: output.setEditable(false);
152: output.setFont(new Font("Monospaced", Font.PLAIN, output
153: .getFont().getSize() + 1)); // NOI18N
154: output.setForeground(UIManager.getColor("Label.foreground")); // NOI18N
155: output.setBackground(UIManager.getColor("Label.background")); // NOI18N
156:
157: setLayout(new BorderLayout());
158: add(new JScrollPane(output));
159: setBorder(new javax.swing.border.BevelBorder(
160: javax.swing.border.BevelBorder.LOWERED));
161:
162: next.getAccessibleContext().setAccessibleDescription(
163: bundle.getString("ACSD_NextException"));
164: previous.getAccessibleContext().setAccessibleDescription(
165: bundle.getString("ACSD_PreviousException"));
166: output.getAccessibleContext().setAccessibleName(
167: bundle.getString("ACSN_ExceptionStackTrace"));
168: output.getAccessibleContext().setAccessibleDescription(
169: bundle.getString("ACSD_ExceptionStackTrace"));
170: getAccessibleContext().setAccessibleDescription(
171: bundle.getString("ACSD_NotifyExceptionPanel"));
172:
173: descriptor = new DialogDescriptor("", ""); // NOI18N
174:
175: descriptor.setMessageType(DialogDescriptor.ERROR_MESSAGE);
176: descriptor.setOptions(computeOptions(previous, next));
177: descriptor.setAdditionalOptions(new Object[] { details });
178: descriptor.setClosingOptions(new Object[0]);
179: descriptor.setButtonListener(this );
180:
181: // bugfix #27176, create dialog in modal state if some other modal
182: // dialog is opened at the time
183: // #53328 do not let the error dialog to be created modal unless the main
184: // window is visible. otherwise the error message may be hidden behind
185: // the main window thus making the main window unusable
186: descriptor.setModal(isModalDialogPresent()
187: && WindowManager.getDefault().getMainWindow()
188: .isVisible());
189:
190: dialog = DialogDisplayer.getDefault().createDialog(descriptor);
191: if (null != lastBounds)
192: dialog.setBounds(lastBounds);
193:
194: dialog.getAccessibleContext().setAccessibleName(
195: bundle.getString("ACN_NotifyExcPanel_Dialog")); // NOI18N
196: dialog.getAccessibleContext().setAccessibleDescription(
197: bundle.getString("ACD_NotifyExcPanel_Dialog")); // NOI18N
198: }
199:
200: static Object[] computeOptions(Object previous, Object next) {
201: ArrayList<Object> arr = new ArrayList<java.lang.Object>();
202: arr.add(previous);
203: arr.add(next);
204:
205: for (Handler h : Logger.getLogger("").getHandlers()) {
206: if (h instanceof Callable<?>) {
207: boolean foundCallableForJButton = false;
208: for (Type t : h.getClass().getGenericInterfaces()) {
209: if (t instanceof ParameterizedType) {
210: ParameterizedType p = (ParameterizedType) t;
211: Type[] params = p.getActualTypeArguments();
212: if (params.length == 1
213: && params[0] == JButton.class) {
214: foundCallableForJButton = true;
215: break;
216: }
217: }
218: }
219: if (!foundCallableForJButton) {
220: continue;
221: }
222:
223: try {
224: Object o = ((Callable<?>) h).call();
225: assert o instanceof JButton;
226: arr.add(o);
227: } catch (Exception ex) {
228: Exceptions.printStackTrace(ex);
229: }
230: }
231: }
232:
233: arr.add(DialogDescriptor.CLOSED_OPTION);
234: return arr.toArray();
235: }
236:
237: private static boolean isModalDialogPresent() {
238: return hasModalDialog(WindowManager.getDefault()
239: .getMainWindow())
240: // XXX Trick to get the shared frame instance.
241: || hasModalDialog(new JDialog().getOwner());
242: }
243:
244: private static boolean hasModalDialog(Window w) {
245: if (w == null) { // #63830
246: return false;
247: }
248: Window[] ws = w.getOwnedWindows();
249: for (int i = 0; i < ws.length; i++) {
250: if (ws[i] instanceof Dialog && ((Dialog) ws[i]).isModal()
251: && ws[i].isVisible()) {
252: return true;
253: } else if (hasModalDialog(ws[i])) {
254: return true;
255: }
256: }
257:
258: return false;
259: }
260:
261: /**
262: * For unit-testing only
263: */
264: static void cleanInstance() {
265: INSTANCE = null;
266: }
267:
268: /** Adds new exception into the queue.
269: */
270: static void notify(final NbErrorManager.Exc t) {
271: if (!shallNotify(t.getSeverity(), false)) {
272: return;
273: }
274:
275: // #50018 Don't try to show any notify dialog when reporting headless exception
276: if ("java.awt.HeadlessException".equals(t.getClassName())
277: && GraphicsEnvironment.isHeadless()) {
278: t.printStackTrace(System.err);
279: return;
280: }
281:
282: SwingUtilities.invokeLater(new Runnable() {
283: public void run() {
284: String glm = t.getLocalizedMessage();
285: Level gs = t.getSeverity();
286: boolean loc = t.isLocalized();
287:
288: if (loc) {
289: if (gs == Level.WARNING) {
290: DialogDisplayer
291: .getDefault()
292: .notify(
293: new NotifyDescriptor.Message(
294: glm,
295: NotifyDescriptor.WARNING_MESSAGE));
296: return;
297: }
298:
299: if (gs.intValue() == 1973) {
300: DialogDisplayer
301: .getDefault()
302: .notify(
303: new NotifyDescriptor.Message(
304: glm,
305: NotifyDescriptor.INFORMATION_MESSAGE));
306: return;
307: }
308:
309: if (gs == Level.SEVERE) {
310: DialogDisplayer
311: .getDefault()
312: .notify(
313: new NotifyDescriptor.Message(
314: glm,
315: NotifyDescriptor.ERROR_MESSAGE));
316: return;
317: }
318: }
319:
320: if (null == exceptions) {
321: exceptions = new ArrayListPos();
322: }
323: exceptions.add(t);
324: exceptions.position = exceptions.size() - 1;
325:
326: if (shallNotify(t.getSeverity(), true)) {
327: // Assertions are on, so show the exception window.
328: if (INSTANCE == null) {
329: INSTANCE = new NotifyExcPanel();
330: }
331: INSTANCE.updateState(t);
332: } else {
333: // No assertions, use the flashing icon.
334: if (null != flasher && null == INSTANCE) {
335: //exception window is not visible, start flashing the icon
336: flasher.setToolTipText(getExceptionSummary(t));
337: flasher.startFlashing();
338: } else {
339: //exception window is already visible (or the flashing icon is not available)
340: //so we'll only update the exception window
341: if (INSTANCE == null) {
342: INSTANCE = new NotifyExcPanel();
343: }
344: INSTANCE.updateState(t);
345: }
346: }
347: }
348: });
349: }
350:
351: /**
352: * @return A brief exception summary for the flashing icon tooltip (either
353: * the exception message or exception class name).
354: */
355: private static String getExceptionSummary(final NbErrorManager.Exc t) {
356: String plainmsg;
357: String glm = t.getLocalizedMessage();
358: if (glm != null) {
359: plainmsg = glm;
360: } else if (t.getMessage() != null) {
361: plainmsg = t.getMessage();
362: } else {
363: plainmsg = t.getClassName();
364: }
365: assert plainmsg != null;
366: return plainmsg;
367: }
368:
369: /**
370: * updates the state of the dialog. called only in AWT thread.
371: */
372: private void updateState(NbErrorManager.Exc t) {
373: if (!exceptions.existsNextElement()) {
374: // it can be commented out while INSTANCE is not cached
375: // (see the comment in actionPerformed)
376: /*// be modal if some modal dialog is already opened, nonmodal otherwise
377: boolean isModalDialogOpened = NbPresenter.currentModalDialog != null;
378: if (descriptor.isModal() != isModalDialogOpened) {
379: descriptor.setModal(isModalDialogOpened);
380: // bugfix #27176, old dialog is disposed before recreating
381: if (dialog != null) dialog.dispose ();
382: // so we can safely send it to gc and recreate dialog
383: // dialog = org.openide.DialogDisplayer.getDefault ().createDialog (descriptor);
384: }*/
385: // the dialog is not shown
386: current = t;
387: update();
388: } else {
389: // add the exception to the queue
390: next.setVisible(true);
391: }
392: try {
393: //Dialog.show() will pump events for the AWT thread. If the
394: //exception happened because of a paint, it will trigger opening
395: //another dialog, which will trigger another exception, endlessly.
396: //Catch any exceptions and append them to the list instead.
397: ensurePreferredSize();
398: dialog.setVisible(true);
399: //throw new RuntimeException ("I am not so exceptional"); //uncomment to test
400: } catch (Exception e) {
401: exceptions.add(NbErrorManager.createExc(e, Level.SEVERE,
402: null));
403: next.setVisible(true);
404: }
405: }
406:
407: private void ensurePreferredSize() {
408: if (null != lastBounds) {
409: return; //we remember the last window position
410: } //we remember the last window position
411: Dimension sz = dialog.getSize();
412: Dimension pref = dialog.getPreferredSize();
413: if (pref.height == 0) {
414: pref.height = SIZE_PREFERRED_HEIGHT;
415: }
416: if (pref.width == 0) {
417: pref.width = SIZE_PREFERRED_WIDTH;
418: }
419: if (!sz.equals(pref)) {
420: dialog.setSize(pref.width, pref.height);
421: dialog.validate();
422: dialog.repaint();
423: }
424: }
425:
426: /** Updates the visual state of the dialog.
427: */
428: private void update() {
429: // JST: this can be improved in future...
430: boolean isLocalized = current.isLocalized();
431:
432: next.setVisible(exceptions.existsNextElement());
433: previous.setVisible(exceptions.existsPreviousElement());
434:
435: if (showDetails) {
436: Mnemonics.setLocalizedText(details,
437: org.openide.util.NbBundle.getBundle(
438: NotifyExcPanel.class).getString(
439: "CTL_Exception_Hide_Details"));
440: details.getAccessibleContext().setAccessibleDescription(
441: org.openide.util.NbBundle.getBundle(
442: NotifyExcPanel.class).getString(
443: "ACSD_Exception_Hide_Details"));
444: } else {
445: Mnemonics.setLocalizedText(details,
446: org.openide.util.NbBundle.getBundle(
447: NotifyExcPanel.class).getString(
448: "CTL_Exception_Show_Details"));
449: details.getAccessibleContext().setAccessibleDescription(
450: org.openide.util.NbBundle.getBundle(
451: NotifyExcPanel.class).getString(
452: "ACSD_Exception_Show_Details"));
453: }
454:
455: // setText (current.getLocalizedMessage ());
456: String title = org.openide.util.NbBundle.getBundle(
457: NotifyExcPanel.class).getString("CTL_Title_Exception");
458:
459: if (showDetails) {
460: descriptor.setMessage(this );
461:
462: SwingUtilities.invokeLater(new Runnable() {
463: public void run() {
464: // XXX #28191: some other piece of code should underline these, etc.
465: StringWriter wr = new StringWriter();
466: current.printStackTrace(new PrintWriter(wr, true));
467: output.setText(wr.toString());
468: output.getCaret().setDot(0);
469: output.requestFocus();
470: }
471: });
472: } else {
473: if (isLocalized) {
474: String msg = current.getLocalizedMessage();
475: if (msg != null) {
476: descriptor.setMessage(msg);
477: }
478: } else {
479: ResourceBundle curBundle = NbBundle
480: .getBundle(NotifyExcPanel.class);
481: if (current.getSeverity() == Level.WARNING) {
482: // less scary message for warning level
483: descriptor.setMessage(java.text.MessageFormat
484: .format(curBundle
485: .getString("NTF_ExceptionWarning"),
486: new Object[] { current
487: .getClassName() }));
488: title = curBundle
489: .getString("NTF_ExceptionWarningTitle"); // NOI18N
490: } else {
491: // emphasize user-non-friendly exceptions
492: // if (this.getMessage() == null || "".equals(this.getMessage())) { // NOI18N
493: descriptor
494: .setMessage(java.text.MessageFormat
495: .format(
496: curBundle
497: .getString("NTF_ExceptionalException"),
498: new Object[] {
499: current
500: .getClassName(),
501: CLIOptions
502: .getLogDir() }));
503:
504: title = curBundle
505: .getString("NTF_ExceptionalExceptionTitle"); // NOI18N
506: }
507: }
508: }
509:
510: descriptor.setTitle(title);
511:
512: }
513:
514: //
515: // Handlers
516: //
517:
518: public void actionPerformed(final java.awt.event.ActionEvent ev) {
519: if (ev.getSource() == next && exceptions.setNextElement()
520: || ev.getSource() == previous
521: && exceptions.setPreviousElement()) {
522: current = exceptions.get();
523: LogRecord rec = new LogRecord(Level.CONFIG,
524: "NotifyExcPanel: " + ev.getActionCommand());// NOI18N
525: String message = current.getMessage();
526: String className = current.getClassName();
527: if (message != null) {
528: className = className + ": " + message;
529: }
530: Object[] params = { className,
531: current.getFirstStacktraceLine() }; // NOI18N
532: rec.setParameters(params);
533: //log changes in NotifyPanel - #119632
534: Logger.getLogger("org.netbeans.ui.NotifyExcPanel").log(rec);// NOI18N
535: update();
536: // bugfix #27266, don't change the dialog's size when jumping Next<->Previous
537: //ensurePreferredSize();
538: return;
539: }
540:
541: if (ev.getSource() == details) {
542: showDetails = !showDetails;
543: lastBounds = null;
544: try {
545: update();
546: ensurePreferredSize();
547: //throw new RuntimeException ("I am reallly exceptional!"); //uncomment to test
548: } catch (Exception e) {
549: //Do not allow an exception thrown here to trigger an endless
550: //loop
551: exceptions.add(NbErrorManager.createExc(e, //ugly but works
552: Level.SEVERE, null));
553: next.setVisible(true);
554: }
555: return;
556: }
557:
558: // bugfix #40834, remove all exceptions to notify when close a dialog
559: if (ev.getSource() == DialogDescriptor.OK_OPTION
560: || ev.getSource() == DialogDescriptor.CLOSED_OPTION) {
561: try {
562: exceptions.removeAll();
563: //Fixed bug #9435, call of setVisible(false) replaced by call of dispose()
564: //It did not work on Linux when JDialog is reused.
565: //dialog.setVisible (false);
566: // XXX(-ttran) no, it still doesn't work, getPreferredSize() on the
567: // reused dialog returns (0,0). We stop caching the dialog
568: // completely by setting INSTANCE to null here.
569: lastBounds = dialog.getBounds();
570: dialog.dispose();
571: exceptions = null;
572: INSTANCE = null;
573: //throw new RuntimeException ("You must be exceptional"); //uncomment to test
574: } catch (RuntimeException e) {
575: //Do not allow window of opportunity when dialog in a possibly
576: //inconsistent state may be reuse
577: exceptions = null;
578: INSTANCE = null;
579: throw e;
580: } finally {
581: exceptions = null;
582: INSTANCE = null;
583: }
584: }
585: }
586:
587: /** Method that checks whether the level is high enough to be notified
588: * at all.
589: * @param dialog shall we check for dialog or just a blinking icon (false)
590: */
591: private static boolean shallNotify(Level level, boolean dialog) {
592: int minAlert = Integer.getInteger(
593: "netbeans.exception.alert.min.level", 900); // NOI18N
594: boolean assertionsOn = false;
595: assert assertionsOn = true;
596: int defReport = assertionsOn ? 900 : 1001;
597: int minReport = Integer.getInteger(
598: "netbeans.exception.report.min.level", defReport); // NOI18N
599:
600: if (dialog) {
601: return level.intValue() >= minReport;
602: } else {
603: return level.intValue() >= minAlert
604: || level.intValue() >= minReport;
605: }
606: }
607:
608: /**
609: * The icon shown in the main status bar that is flashing when an exception
610: * is encountered.
611: */
612: static FlashingIcon flasher = null;
613:
614: /**
615: * Return an icon that is flashing when a new internal exception occurs.
616: * Clicking the icon opens the regular exception dialog box. The icon
617: * disappears (is hidden) after a short period of time and the exception
618: * list is cleared.
619: *
620: * @return A flashing icon component or null if console logging is switched on.
621: */
622: public static Component getNotificationVisualizer() {
623: //do not create flashing icon if not allowed in system properties
624: if (null == flasher) {
625: ImageIcon img1 = new ImageIcon(Utilities.loadImage(
626: "org/netbeans/core/resources/exception.gif", true));
627: flasher = new ExceptionFlasher(img1);
628: }
629: return flasher;
630: }
631:
632: private static class ExceptionFlasher extends FlashingIcon {
633: static final long serialVersionUID = 1L;
634:
635: public ExceptionFlasher(Icon img1) {
636: super (img1);
637: }
638:
639: /**
640: * User clicked the flashing icon, display the exception window.
641: */
642: protected void onMouseClick() {
643: if (null != exceptions && exceptions.size() > 0) {
644: if (INSTANCE == null) {
645: INSTANCE = new NotifyExcPanel();
646: }
647: INSTANCE.updateState(exceptions
648: .get(exceptions.size() - 1));
649: }
650: }
651:
652: /**
653: * The flashing icon disappeared (timed-out), clear the current
654: * exception list.
655: */
656: protected void timeout() {
657: SwingUtilities.invokeLater(new Runnable() {
658: public void run() {
659: if (null != INSTANCE) {
660: return;
661: }
662: if (null != exceptions) {
663: exceptions.clear();
664: }
665: exceptions = null;
666: }
667: });
668: }
669: }
670:
671: static class ArrayListPos extends ArrayList<NbErrorManager.Exc> {
672: static final long serialVersionUID = 2L;
673:
674: protected int position;
675:
676: protected ArrayListPos() {
677: super ();
678: position = 0;
679: }
680:
681: protected boolean existsElement() {
682: return size() > 0;
683: }
684:
685: protected boolean existsNextElement() {
686: return position + 1 < size();
687: }
688:
689: protected boolean existsPreviousElement() {
690: return position > 0 && size() > 0;
691: }
692:
693: protected boolean setNextElement() {
694: if (!existsNextElement()) {
695: return false;
696: }
697: position++;
698: return true;
699: }
700:
701: protected boolean setPreviousElement() {
702: if (!existsPreviousElement()) {
703: return false;
704: }
705: position--;
706: return true;
707: }
708:
709: protected NbErrorManager.Exc get() {
710: return existsElement() ? get(position) : null;
711: }
712:
713: protected void removeAll() {
714: clear();
715: position = 0;
716: }
717: }
718: }
|