Let's say you create a marvelously successful plug-in. You have thousands—no millions—of satisfied customers. What a disaster!
Why a disaster? Even with test-driven plug-in development some percentage of users will have problems. The more users, the more problems. Pretty soon, support costs will eat you out of house and home.
In this chapter we'll talk about Eclipse features that collectively reduce support costs:
Handling Eclipse exceptions
Presenting exceptions to the user using an error dialog
Logging errors to the Eclipse log
An important rule to improve the serviceability of your plug-ins is a bit of a paradox:
RESPONSIBILITY RULE. Clearly identify your plug-in as the source of problems.
How can telling the user a mistake is yours reduce your cost of service?
If you have an open source plug-in, the community will collectively provide first level service. If you provide the necessary information in log entries and error dialogs, most problems will be solved before you ever see them.
If you know problems can be traced directly to your plug-in, you will be careful to write robust code. The test-driven techniques introduced in the Interlude can go far towards eliminating problems at the source if applied carefully.
To address the Responsibility Rule, exceptions thrown by plug-ins have to carry an IStatus
object. The IStatus
object identifies the plug-in and contains a severity, a status code, a message, and optionally, an exception. The contained exception typically comes from a lower layer that should not be exposed to clients. CoreException
is the root of the Eclipse exception hierarchy and carries an IStatus
.
When an exception occurs, you want to inform the user that it happened. If you cannot show the exception to the user, then you want to at least log it to help you service problem reports. For example, it is always safe to inform the user of an exception when it happens in your action code. Then you know the context the user is in, that is, they just invoked the action. The situation is less clear when you don't know the calling context. You have to decide whether you want to propagate the exception, inform the user, or to just log the exception.
Example . org.eclipse.ui.internal.actions/OpenWorkspaceFileAction
public void run(IAction action) {
//...
try {
IWorkbenchPage page = workbenchWindow.getActivePage();
if (page != null)
page.openEditor(file);
} catch (CoreException x) {
String title = "Open File"
String message = "An Exception occurred while "+
"opening the resource";
WorkbenchPlugin.log(title, x.getStatus());
ErrorDialog.openError(workbenchWindow.getShell(), title,
message, x.getStatus());
}
}
When a CoreException
happens, your first decision is whether you want to inform the user with a dialog or whether you want to just log the problem. If you are in a context where you can show a dialog, then you should always do so. If this is not possible, you should log the problem. If you have informed the user of the problem with a dialog, there is no need to also log the problem unless you feel it will help you service problem reports later. Never just swallow an exception. In the snippet above, the developer has decided to both inform the user and to log the problem.
The Plugin
class provides access to the log. It is common to provide a static helper method returning the log. The helper takes an IStatus
object and passes it on to the log as shown below:
To inform the user about a problem Eclipse provides an ErrorDialog
that can present the IStatus
to the user. It is used in the snippet above.
Here are some other error situations and hints for how to do the error reporting:
You don't have a CoreException
or an exception carrying an IStatus
—. Inform the user with the MessageDialog
(MessageDialog.openError()
). To log the exception create an IStatus
object and log it.
A single conceptual operation can have multiple errors and you do want to proceed despite a single error—. In this case, collect all of the IStatus
' in a MultiStatus
and pass the MuliStatus
to the ErrorDialog
or the log.
Let's apply what we have learned to our example. A good place to use an error dialog in our code is the AutoTestPropertyPage
. When the user clicks OK, our method performOk()
is called. The methods we call, like JUnitPlugin.addAutoTestNature()
, can throw a CoreException
. Opening an error dialog makes perfect sense, since we're in the middle of a user interface operation:
Example . org.eclipse.contribution.junit/AutoTestPropertyPage
public boolean performOk() { try { JUnitPlugin plugin= JUnitPlugin.getPlugin(); if (autoTest.getSelection()) plugin.addAutoBuildNature(getProject()); else plugin.removeAutoBuildNature(getProject()); } catch (CoreException e) { ErrorDialog.openError(getShell(), "Error", "Cannot set auto-test property", e.getStatus()); } return true; }
We can force this dialog to appear by throwing an exception in the body of the try
block.
Example . org.eclipse.contribution.junit/AutoTestPropertyPage
public boolean performOk() { try { JUnitPlugin plugin= JUnitPlugin.getPlugin(); Exception exception= null; String id= plugin.getDescriptor().getUniqueIdentifier(); int code= 42; IStatus status= new Status(IStatus.ERROR, id, code, "Status message", exception); throw new CoreException(status); } catch (CoreException e) { ErrorDialog.openError(getShell(), "Error", "Cannot set auto-test property", e.getStatus()); } return true; }
Then we see the error dialog shown in Figure 20.1.
This code also serves to show you how to throw a CoreException
. As described above, each CoreException
has associated with it an IStatus
, an object that bundles information about an operation. We're using it to report a problem (IStatus.ERROR
). The status also carries a human-readable error message (you can see it in the dialog above), an error code, and the original exception, if any.
We left several TODO comments about places in the code where we wanted to do a more thorough job of error handling. We'll fill in one of those places here.
In MarkerCreator
, we had to clear the test failure markers any time a project's tests restarted. Removing the markers potentially throws a CoreException
. In this case, we cannot make any assumption about the calling context of this method, so we cannot simply show an ErrorDialog
. Therefore we just log the problem:
Example . org.eclipse.contribution.junit/MarkerCreator$Listener
public void testsStarted(IJavaProject project, int testCount) { try { IResource resource= project.getUnderlyingResource(); resource.deleteMarkers( "org.eclipse.contribution.junit.failure", false, IResource.DEPTH_INFINITE); } catch (CoreException e) { JUnitPlugin plugin= JUnitPlugin.getPlugin(); IStatus status= new Status(IStatus.ERROR, plugin.getDescriptor().getUniqueIdentifier(), 0, "Problem deleting markers", e); plugin.getLog().log(status); } }
Now when we force the problem to occur:
Example . org.eclipse.contribution.junit/MarkerCreator$Listener
public void testsStarted(IJavaProject project, int testCount) { JUnitPlugin plugin= JUnitPlugin.getPlugin(); try { IStatus status= new Status(IStatus.ERROR, plugin().getDescriptor().getUniqueIdentifier(), 42, "Status message", null); throw new CoreException(status); } catch (CoreException e) { IStatus status= new Status(IStatus.ERROR, plugin.getDescriptor().getUniqueIdentifier(), 0, "Problem deleting markers", e); plugin.getLog().log(status); } }
We get the error log entry shown in Figure 20.2. To see the entry we first open the Error Log view, shown in Figure 20.3.
Bringing up the properties of the entry, we see the stack trace including our error messages, shown in Figure 20.4.
In addition to browsing the log with the Error Log view, you can always access the log file directly. It is located in the target workspace's .metadata folder with the name .log.
Another aspect of reducing the cost of servicing your plug-ins is instrumenting your plug-in with code that can be optionally enabled. We'll cover that in the next chapter. Reviewing this chapter, we
Opened an error dialog when we received a CoreException
during a user interface operation
Wrote a message to the log file when we received a CoreException
in a context where we didn't want to show a dialog