blob: a809f09a91b3ea14ffa9eeb223a7cc49643b207f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.core;
import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.Desktop;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GraphicsEnvironment;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.ResourceBundle;
import java.util.concurrent.Callable;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.swing.BorderFactory;
import javax.swing.FocusManager;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.event.HyperlinkEvent;
import org.netbeans.core.startup.CLIOptions;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.awt.Mnemonics;
import org.openide.awt.Notification;
import org.openide.awt.NotificationDisplayer;
import org.openide.util.Exceptions;
import org.openide.util.ImageUtilities;
import org.openide.util.NbBundle;
import org.openide.windows.WindowManager;
/**
* Notifies exceptions.
*
* This class is public only because the MainWindow needs get the flashing
* icon to its status bar from this class (method getNotificationVisualizer()).
*
* @author Jaroslav Tulach
*/
public final class NotifyExcPanel extends JPanel implements ActionListener {
static final long serialVersionUID =3680397500573480127L;
/** the instance */
private static NotifyExcPanel INSTANCE = null;
/** preferred width of this component */
private static final int SIZE_PREFERRED_WIDTH=550;
/** preferred height of this component */
private static final int SIZE_PREFERRED_HEIGHT=350;
private static final int MAX_STORED_EXCEPTIONS = 500;
private static final boolean AUTO_FOCUS = Boolean.getBoolean("netbeans.winsys.auto_focus"); // NOI18N
/** enumeration of NbExceptionManager.Exc to notify */
static ArrayListPos exceptions;
/** current exception */
private NbErrorManager.Exc current;
/** dialog descriptor */
private DialogDescriptor descriptor;
/** dialog that displayes the exceptions */
java.awt.Dialog dialog;
/** button to show next exceptions */
private JButton next;
/** button to show previous exceptions */
private JButton previous;
/** details button */
private JButton details;
/** details window */
private JTextArea output;
/** boolean to show/hide details */
private static boolean showDetails;
/** the last position of the exception dialog window */
private static Rectangle lastBounds;
private static int extraH = 0, extraW = 0;
/** Constructor.
*/
private NotifyExcPanel () {
java.util.ResourceBundle bundle = org.openide.util.NbBundle.getBundle(NotifyExcPanel.class);
next = new JButton ();
Mnemonics.setLocalizedText(next, bundle.getString("CTL_NextException"));
// bugfix 25684, don't set Previous/Next as default capable
next.setDefaultCapable (false);
previous = new JButton ();
Mnemonics.setLocalizedText(previous, bundle.getString("CTL_PreviousException"));
previous.setDefaultCapable (false);
details = new JButton ();
details.setDefaultCapable (false);
output = new JTextArea() {
public @Override boolean getScrollableTracksViewportWidth() {
return false;
}
};
output.setEditable(false);
output.setLineWrap(false);
Font f = output.getFont();
output.setFont(new Font("Monospaced", Font.PLAIN, null == f ? 12 : f.getSize() + 1)); // NOI18N
output.setForeground(UIManager.getColor("Label.foreground")); // NOI18N
output.setBackground(UIManager.getColor("Label.background")); // NOI18N
setLayout( new BorderLayout() );
add(new JScrollPane(output));
setBorder( new javax.swing.border.BevelBorder(javax.swing.border.BevelBorder.LOWERED));
next.getAccessibleContext().setAccessibleDescription(bundle.getString("ACSD_NextException"));
previous.getAccessibleContext().setAccessibleDescription(bundle.getString("ACSD_PreviousException"));
output.getAccessibleContext().setAccessibleName(bundle.getString("ACSN_ExceptionStackTrace"));
output.getAccessibleContext().setAccessibleDescription(bundle.getString("ACSD_ExceptionStackTrace"));
getAccessibleContext().setAccessibleDescription(bundle.getString("ACSD_NotifyExceptionPanel"));
descriptor = new DialogDescriptor ("", ""); // NOI18N
descriptor.setMessageType (DialogDescriptor.ERROR_MESSAGE);
descriptor.setOptions (computeOptions(previous, next));
descriptor.setAdditionalOptions (new Object[] {
details
});
descriptor.setClosingOptions (new Object[0]);
descriptor.setButtonListener (this);
// bugfix #27176, create dialog in modal state if some other modal
// dialog is opened at the time
// #53328 do not let the error dialog to be created modal unless the main
// window is visible. otherwise the error message may be hidden behind
// the main window thus making the main window unusable
descriptor.setModal( isModalDialogPresent()
&& WindowManager.getDefault().getMainWindow().isVisible() );
setPreferredSize(new Dimension(SIZE_PREFERRED_WIDTH + extraW, SIZE_PREFERRED_HEIGHT + extraH));
dialog = DialogDisplayer.getDefault().createDialog(descriptor);
if( null != lastBounds ) {
lastBounds.width = Math.max( lastBounds.width, SIZE_PREFERRED_WIDTH+extraW );
dialog.setBounds( lastBounds );
}
dialog.getAccessibleContext().setAccessibleName(bundle.getString("ACN_NotifyExcPanel_Dialog")); // NOI18N
dialog.getAccessibleContext().setAccessibleDescription(bundle.getString("ACD_NotifyExcPanel_Dialog")); // NOI18N
}
static Object[] computeOptions(Object previous, Object next) {
ArrayList<Object> arr = new ArrayList<java.lang.Object>();
arr.add(previous);
arr.add(next);
extraH = 0;
extraW = 0;
for (Handler h : Logger.getLogger("").getHandlers()) {
if (h instanceof Callable<?>) {
boolean foundCallableForJButton = false;
for (Type t : h.getClass().getGenericInterfaces()) {
if (t instanceof ParameterizedType) {
ParameterizedType p = (ParameterizedType)t;
Type[] params = p.getActualTypeArguments();
if (params.length == 1 && params[0] == JButton.class) {
foundCallableForJButton = true;
break;
}
}
}
if (!foundCallableForJButton) {
continue;
}
try {
Object o = ((Callable<?>)h).call();
if (o == null) {
continue;
}
assert o instanceof JButton;
JButton b = (JButton) o;
extraH += b.getPreferredSize ().height;
extraW += b.getPreferredSize ().width;
arr.add(o);
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
}
}
arr.add(NotifyDescriptor.CANCEL_OPTION);
return arr.toArray();
}
private static boolean isModalDialogPresent() {
return hasModalDialog(WindowManager.getDefault().getMainWindow())
// XXX Trick to get the shared frame instance.
|| hasModalDialog(new JDialog().getOwner());
}
private static boolean hasModalDialog(Window w) {
if (w == null) { // #63830
return false;
}
Window[] ws = w.getOwnedWindows();
for(int i = 0; i < ws.length; i++) {
if(ws[i] instanceof Dialog && ((Dialog)ws[i]).isModal() && ws[i].isVisible()) {
return true;
} else if(hasModalDialog(ws[i])) {
return true;
}
}
return false;
}
/**
* For unit-testing only
*/
static void cleanInstance() {
INSTANCE = null;
}
/** Adds new exception into the queue.
*/
static void notify (
final NbErrorManager.Exc t
) {
if (!t.isUserQuestion() && !shallNotify(t.getSeverity(), false)) {
return;
}
// #50018 Don't try to show any notify dialog when reporting headless exception
if (/*"java.awt.HeadlessException".equals(t.getClassName()) &&*/ GraphicsEnvironment.isHeadless()) { // NOI18N
t.printStackTrace(System.err);
return;
}
SwingUtilities.invokeLater (new Runnable () {
@Override
public void run() {
String glm = t.getLocalizedMessage();
Level gs = t.getSeverity();
boolean loc = t.isLocalized();
if (t.isUserQuestion() && loc) {
Object ret = DialogDisplayer.getDefault().notify(
new NotifyDescriptor.Confirmation(glm, NotifyDescriptor.OK_CANCEL_OPTION));
if (ret == NotifyDescriptor.OK_OPTION) {
try {
t.confirm();
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
return;
}
if (loc) {
if (gs == Level.WARNING) {
DialogDisplayer.getDefault().notify(
new NotifyDescriptor.Message(glm, NotifyDescriptor.WARNING_MESSAGE)
);
return;
}
if (gs.intValue() == 1973) {
DialogDisplayer.getDefault().notify(
new NotifyDescriptor.Message(glm, NotifyDescriptor.INFORMATION_MESSAGE)
);
return;
}
if (gs == Level.SEVERE) {
DialogDisplayer.getDefault().notify(
new NotifyDescriptor.Message(glm, NotifyDescriptor.ERROR_MESSAGE)
);
return;
}
}
if( null == exceptions ) {
exceptions = new ArrayListPos();
} else if (exceptions.size() >= MAX_STORED_EXCEPTIONS) {
// Ignore huge number of exceptions, prevents from OOME.
return ;
}
exceptions.add(t);
exceptions.position = exceptions.size()-1;
if(shallNotify(t.getSeverity(), true)) {
// Assertions are on, so show the exception window.
if( INSTANCE == null ) {
INSTANCE = new NotifyExcPanel();
}
INSTANCE.updateState(t);
} else {
// No assertions, use the flashing icon.
if( null == INSTANCE ) {
ImageIcon img1 = ImageUtilities.loadImageIcon("org/netbeans/core/resources/exception.gif", true);
String summary = getExceptionSummary(t);
ExceptionFlasher flash = ExceptionFlasher.notify(summary, img1);
//exception window is not visible, start flashing the icon
} else {
//exception window is already visible (or the flashing icon is not available)
//so we'll only update the exception window
if( INSTANCE == null ) {
INSTANCE = new NotifyExcPanel();
}
INSTANCE.updateState(t);
}
}
}
});
}
/**
* @return A brief exception summary for the flashing icon tooltip (either
* the exception message or exception class name).
*/
private static String getExceptionSummary( final NbErrorManager.Exc t ) {
String plainmsg;
String glm = t.getLocalizedMessage();
if (glm != null) {
plainmsg = glm;
} else if (t.getMessage() != null) {
plainmsg = t.getMessage();
} else {
plainmsg = t.getClassName();
}
assert plainmsg != null;
return plainmsg;
}
/**
* updates the state of the dialog. called only in AWT thread.
*/
private void updateState (NbErrorManager.Exc t) {
if (!exceptions.existsNextElement()) {
// it can be commented out while INSTANCE is not cached
// (see the comment in actionPerformed)
/*// be modal if some modal dialog is already opened, nonmodal otherwise
boolean isModalDialogOpened = NbPresenter.currentModalDialog != null;
if (descriptor.isModal() != isModalDialogOpened) {
descriptor.setModal(isModalDialogOpened);
// bugfix #27176, old dialog is disposed before recreating
if (dialog != null) dialog.dispose ();
// so we can safely send it to gc and recreate dialog
// dialog = org.openide.DialogDisplayer.getDefault ().createDialog (descriptor);
}*/
// the dialog is not shown
current = t;
update ();
} else {
// add the exception to the queue
next.setVisible (true);
dialog.pack();
}
try {
//Dialog.show() will pump events for the AWT thread. If the
//exception happened because of a paint, it will trigger opening
//another dialog, which will trigger another exception, endlessly.
//Catch any exceptions and append them to the list instead.
ensurePreferredSize();
if (!dialog.isVisible()) {
dialog.setVisible(true);
}
//throw new RuntimeException ("I am not so exceptional"); //uncomment to test
} catch (Exception e) {
exceptions.add(NbErrorManager.createExc(
e, Level.SEVERE, null));
next.setVisible(true);
}
}
private void ensurePreferredSize() {
if( null != lastBounds ) {
return; //we remember the last window position
} //we remember the last window position
Dimension sz = dialog.getSize();
Dimension pref = dialog.getPreferredSize();
if (pref.height == 0) {
pref.height = SIZE_PREFERRED_HEIGHT;
}
if (pref.width == 0) {
pref.width = SIZE_PREFERRED_WIDTH;
}
if (!sz.equals(pref)) {
dialog.setSize(pref.width, pref.height);
dialog.validate();
dialog.repaint();
}
}
/** Updates the visual state of the dialog.
*/
private void update () {
// JST: this can be improved in future...
boolean isLocalized = current.isLocalized();
boolean repack;
boolean visNext = next.isVisible();
boolean visPrev = previous.isVisible();
next.setVisible (exceptions.existsNextElement());
previous.setVisible (exceptions.existsPreviousElement());
repack = next.isVisible() != visNext || previous.isVisible() != visPrev;
if (showDetails) {
Mnemonics.setLocalizedText(details, org.openide.util.NbBundle.getBundle(NotifyExcPanel.class).getString("CTL_Exception_Hide_Details"));
details.getAccessibleContext().setAccessibleDescription(
org.openide.util.NbBundle.getBundle(NotifyExcPanel.class).getString("ACSD_Exception_Hide_Details"));
} else {
Mnemonics.setLocalizedText(details, org.openide.util.NbBundle.getBundle(NotifyExcPanel.class).getString("CTL_Exception_Show_Details"));
details.getAccessibleContext().setAccessibleDescription(
org.openide.util.NbBundle.getBundle(NotifyExcPanel.class).getString("ACSD_Exception_Show_Details"));
}
// setText (current.getLocalizedMessage ());
String title = org.openide.util.NbBundle.getBundle(NotifyExcPanel.class).getString("CTL_Title_Exception");
if (showDetails) {
descriptor.setMessage (this);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// XXX #28191: some other piece of code should underline these, etc.
StringWriter wr = new StringWriter();
current.printStackTrace(new PrintWriter(wr, true));
output.setText(wr.toString());
output.getCaret().setDot(0);
if (!AUTO_FOCUS && FocusManager.getCurrentManager().getActiveWindow() == null) {
// Do not steal focus if no Java window have it
output.requestFocusInWindow();
} else {
output.requestFocus ();
}
}
});
} else {
if (isLocalized) {
String msg = current.getLocalizedMessage ();
if (msg != null) {
descriptor.setMessage (msg);
}
} else {
ResourceBundle curBundle = NbBundle.getBundle (NotifyExcPanel.class);
String message;
if (current.getSeverity() == Level.WARNING) {
// less scary message for warning level
message = MessageFormat.format(
curBundle.getString("NTF_ExceptionWarning"),
new Object[] { current.getClassName() }
);
title = curBundle.getString("NTF_ExceptionWarningTitle"); // NOI18N
} else {
message = MessageFormat.format(
curBundle.getString("NTF_ExceptionalException"),
new Object[] { current.getClassName(), Paths.get(CLIOptions.getLogDir()).toUri() }
);
title = curBundle.getString("NTF_ExceptionalExceptionTitle"); // NOI18N
}
JTextPane pane = new JTextPane();
pane.setContentType("text/html"); // NOI18N
pane.setText(message);
pane.setBackground(UIManager.getColor("Label.background")); // NOI18N
pane.setBorder(BorderFactory.createEmptyBorder());
pane.setEditable(false);
pane.setFocusable(true);
pane.addHyperlinkListener((e) -> {
if (e.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) {
try {
Desktop.getDesktop().browse(e.getURL().toURI());
} catch (IOException | URISyntaxException ex) {
Exceptions.printStackTrace(ex);
}
}
});
JScrollPane sp = new JScrollPane(pane);
sp.setBorder(BorderFactory.createEmptyBorder());
sp.setPreferredSize(new Dimension(300, 120));
descriptor.setMessage(sp);
}
}
descriptor.setTitle (title);
if (repack) {
dialog.pack();
}
}
//
// Handlers
//
public void actionPerformed(final java.awt.event.ActionEvent ev) {
Object source = ev.getSource();
if (source == next && exceptions.setNextElement() || source == previous && exceptions.setPreviousElement()) {
current = exceptions.get();
LogRecord rec = new LogRecord(Level.CONFIG, "NotifyExcPanel: " + ev.getActionCommand());// NOI18N
String message = current.getMessage();
String className = current.getClassName();
if (message != null){
className = className+": "+ message;
}
Object[] params = {className, current.getFirstStacktraceLine()}; // NOI18N
rec.setParameters(params);
//log changes in NotifyPanel - #119632
Logger.getLogger("org.netbeans.ui.NotifyExcPanel").log(rec);// NOI18N
update ();
// bugfix #27266, don't change the dialog's size when jumping Next<->Previous
//ensurePreferredSize();
return;
}
if (source == details) {
showDetails = !showDetails;
lastBounds = null;
try {
update ();
ensurePreferredSize();
//throw new RuntimeException ("I am reallly exceptional!"); //uncomment to test
} catch (Exception e) {
//Do not allow an exception thrown here to trigger an endless
//loop
exceptions.add(NbErrorManager.createExc(e, //ugly but works
Level.SEVERE, null));
next.setVisible(true);
}
return;
}
// bugfix #40834, remove all exceptions to notify when close a dialog
if (source == NotifyDescriptor.OK_OPTION || source == NotifyDescriptor.CLOSED_OPTION || source == NotifyDescriptor.CANCEL_OPTION) {
LogRecord rec = new LogRecord(Level.CONFIG, "NotifyExcPanel: close");// NOI18N
rec.setParameters(null);
//log changes in NotifyPanel - dialog is closed - forget previous params
Logger.getLogger("org.netbeans.ui.NotifyExcPanel").log(rec);// NOI18N
try {
if( null != exceptions )
exceptions.removeAll();
//Fixed bug #9435, call of setVisible(false) replaced by call of dispose()
//It did not work on Linux when JDialog is reused.
//dialog.setVisible (false);
// XXX(-ttran) no, it still doesn't work, getPreferredSize() on the
// reused dialog returns (0,0). We stop caching the dialog
// completely by setting INSTANCE to null here.
lastBounds = dialog.getBounds();
dialog.dispose();
exceptions = null;
INSTANCE = null;
//throw new RuntimeException ("You must be exceptional"); //uncomment to test
} catch (RuntimeException e) {
//Do not allow window of opportunity when dialog in a possibly
//inconsistent state may be reuse
exceptions = null;
INSTANCE = null;
throw e;
} finally {
exceptions = null;
INSTANCE = null;
}
}
}
/** Method that checks whether the level is high enough to be notified
* at all.
* @param dialog shall we check for dialog or just a blinking icon (false)
*/
private static boolean shallNotify(Level level, boolean dialog) {
int minAlert = Integer.getInteger("netbeans.exception.alert.min.level", 900); // NOI18N
int defReport = 1001;
int minReport = Integer.getInteger("netbeans.exception.report.min.level", defReport); // NOI18N
if (dialog) {
return level.intValue() >= minReport;
} else {
return level.intValue() >= minAlert || level.intValue() >= minReport;
}
}
static class ExceptionFlasher implements ActionListener {
static ExceptionFlasher flash;
private static synchronized ExceptionFlasher notify(String summary, ImageIcon icon) {
if (flash == null) {
flash = new ExceptionFlasher();
} else {
flash.timer.restart();
if (flash.note != null) {
flash.note.clear();
}
}
JComponent detailsPanel = getDetailsPanel(summary);
JComponent bubblePanel = getDetailsPanel(summary);
flash.note = NotificationDisplayer.getDefault().notify(
NbBundle.getMessage(NotifyExcPanel.class, "NTF_ExceptionalExceptionTitle"),
icon, bubblePanel, detailsPanel,
NotificationDisplayer.Priority.SILENT, NotificationDisplayer.Category.ERROR);
return flash;
}
Notification note;
private final Timer timer;
public ExceptionFlasher() {
timer = new Timer(300000, this);
timer.setRepeats(false);
timer.start();
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == timer) {
timeout();
return;
}
synchronized (ExceptionFlasher.class) {
if (note != null) {
note.clear();
}
flash = null;
}
if (null != exceptions && exceptions.size() > 0) {
if (INSTANCE == null) {
INSTANCE = new NotifyExcPanel();
}
INSTANCE.updateState(exceptions.get(exceptions.size() - 1));
}
}
private void timeout() {
synchronized (ExceptionFlasher.class) {
assert EventQueue.isDispatchThread();
if( null != INSTANCE ) {
return;
}
if( null != exceptions ) {
exceptions.clear();
}
exceptions = null;
flash = null;
timer.stop();
if( null != note ) {
note.clear();
}
}
}
private static JComponent getDetailsPanel(String summary) {
JPanel details = new JPanel(new GridBagLayout());
details.setOpaque(false);
JLabel lblMessage = new JLabel(summary);
JButton reportLink = new JButton("<html><a href=\"_blank\">" + NbBundle.getMessage(NotifyExcPanel.class, "NTF_ExceptionalExceptionReport")); //NOI18N
reportLink.setFocusable(false);
reportLink.setBorder(BorderFactory.createEmptyBorder());
reportLink.setBorderPainted(false);
reportLink.setFocusPainted(false);
reportLink.setOpaque(false);
reportLink.setContentAreaFilled(false);
reportLink.addActionListener(flash);
reportLink.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
details.add(reportLink, new GridBagConstraints(0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(3, 0, 3, 0), 0, 0));
details.add(lblMessage, new GridBagConstraints(0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(3, 0, 3, 0), 0, 0));
return details;
}
}
static class ArrayListPos extends ArrayList<NbErrorManager.Exc> {
static final long serialVersionUID = 2L;
static final int SOFT_MAX_SIZE = 20;
static final int HARD_MAX_SIZE = 100; // To prevent from OOME when too many exceptions are thrown
protected int position;
protected ArrayListPos () {
super();
position=0;
}
@Override
public boolean add(NbErrorManager.Exc e) {
if (size() >= SOFT_MAX_SIZE && position < size() - 5) {
set(size() - 1, e);
return true;
} else {
if (size() >= HARD_MAX_SIZE) {
remove(5); // it's beneficient to see the initial exceptions
}
return super.add(e);
}
}
protected boolean existsElement () {
return size()>0;
}
protected boolean existsNextElement () {
return position+1<size();
}
protected boolean existsPreviousElement () {
return position>0&&size()>0;
}
protected boolean setNextElement () {
if(!existsNextElement()) {
return false;
}
position++;
return true;
}
protected boolean setPreviousElement () {
if(!existsPreviousElement()) {
return false;
}
position--;
return true;
}
protected NbErrorManager.Exc get () {
return existsElement()?get(position):null;
}
protected void removeAll () {
clear();
position=0;
}
}
}